Event Bus and Observer Pattern in C# (.NET): From Theory to Practice
In modern C# (.NET) development, one of the key principles is loose coupling between system components. The Observer pattern and its evolution — the Event Bus — provide an elegant mechanism for organizing interaction between objects without direct dependencies. In this article, we will thoroughly examine how to implement an Event Bus in C# (.NET), review its architecture, and provide practical examples.
What is the Observer Pattern?
The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes its state, all dependent objects (observers) are automatically notified and updated. In C#, this pattern is traditionally implemented using events (event) and delegates.
The Problem with the Classic Implementation
Although built-in C# events work well, they have limitations:
- Direct coupling: the publisher must explicitly know the subscribers.
- Unsubscription complexity: if a subscriber forgets to unsubscribe, a memory leak occurs.
- Limited flexibility: it is difficult to organize asynchronous processing or event filtering.
What is an Event Bus?
An Event Bus is a centralized bus (mediator) that implements the Publisher-Subscriber pattern. Unlike the classic Observer, the publisher and subscriber do not know about each other at all. They communicate only through the bus:
- Publisher sends a message (event) to the bus.
- Bus finds all subscribers for this event type and notifies them.
- Subscriber registers for a specific event type and handles it.
Advantages of Event Bus
- Loose coupling: components do not depend on each other.
- Scalability: easy to add new subscribers.
- Testability: the bus can be replaced with mocks.
- Asynchrony: events can be processed in background threads.
Implementing an Event Bus in C# (.NET)
Let's consider a simple but functional implementation. We will create an EventBus class that stores a dictionary of subscribers.
public interface IEventBus{ void Publish<TEvent>(TEvent eventData) where TEvent : class; IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;}
public class EventBus : IEventBus{ private readonly Dictionary<Type, List<Delegate>> _subscribers = new(); private readonly object _lock = new();
public void Publish<TEvent>(TEvent eventData) where TEvent : class { List<Delegate> handlers; lock (_lock) { if (!_subscribers.TryGetValue(typeof(TEvent), out var list)) return; handlers = new List<Delegate>(list); }
foreach (var handler in handlers) { if (handler is Action<TEvent> action) action(eventData); } }
public IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class { lock (_lock) { if (!_subscribers.ContainsKey(typeof(TEvent))) _subscribers[typeof(TEvent)] = new List<Delegate>(); _subscribers[typeof(TEvent)].Add(handler); }
return new Unsubscriber(() => { lock (_lock) { if (_subscribers.TryGetValue(typeof(TEvent), out var list)) list.Remove(handler); } Also in library
Sending Email in JavaScript (Node.js): nodemailer, sendgrid, mailgun
JWT Tokens in Swift: Complete Guide to Working with JSON Web Tokens
Redis client for PHP: Predis — installation, connection, examples
Kotlin Application Configuration: Libraries and Approaches
SQLDelight for Kotlin: Complete Guide with Examples