Event Bus and Observer Pattern in C# (.NET): From Theory to Implementation
In modern C# (.NET) development, there is often a need to organize interaction between system components while maintaining their independence. The Observer pattern and its evolution — the Event Bus — are key tools for implementing loose coupling. In this article, we will break down how these mechanisms work and create a simple yet functional Event Bus implementation in C#.
Observer Pattern: The Classic Approach
The Observer pattern defines a "one-to-many" relationship between objects: when one object (the publisher) changes its state, all dependent objects (subscribers) are automatically notified. In .NET, this pattern is built in through events (event) and delegates.
Disadvantages of the classic implementation:
- Direct dependency of the subscriber on the publisher (a reference to the object must be stored).
- Complexity of managing subscriptions in large systems.
- Threading issues (multithreading requires additional synchronization).
Event Bus: A Centralized Mediator
The Event Bus solves these problems by acting as a central mediator. Instead of subscribing directly to a specific object, components register with the event bus. Publishers publish events to the bus, and the bus distributes them to all subscribers. This implements the Pub/Sub (Publish/Subscribe) pattern.
Advantages of Event Bus:
- Loose coupling: publishers and subscribers know nothing about each other.
- Scalability: easy to add new subscribers.
- Versatility: suitable for GUI (WPF/WinForms), microservices, and games (Unity).
Event Bus Implementation in C# (.NET)
We will create a basic Event Bus implementation that supports asynchronous processing and typed events. For this, we will use the IEvent interface and a generic EventBus class.
Step 1: Define the base types.
// Base interface for all eventspublic interface IEvent { }
// Example event - user messagepublic class UserCreatedEvent : IEvent{ public int UserId { get; } public string Email { get; }
public UserCreatedEvent(int userId, string email) { UserId = userId; Email = email; }}Step 2: Create the event bus.
using System;using System.Collections.Concurrent;using System.Threading.Tasks;
public class EventBus{ // Subscriber storage: key - event type, value - list of handlers private readonly ConcurrentDictionary<Type, ConcurrentBag<object>> _subscribers = new();
// Subscribe to an event public void Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : IEvent { var eventType = typeof(TEvent); var handlers = _subscribers.GetOrAdd(eventType, _ => new ConcurrentBag<object>()); handlers.Add(handler); }
// Unsubscribe (simplified - removes all handlers of this type) public void Unsubscribe<TEvent>() where TEvent : IEvent { _subscribers.TryRemove(typeof(TEvent), out _); }
// Publish an event (asynchronous) public async Task PublishAsync<TEvent>(TEvent eventData) where TEvent : IEvent { if (!_subscribers.TryGetValue(typeof(TEvent), out var handlers)) return;
foreach (var handler in handlers) { if (handler is Func<TEvent, Task> typedHandler) { await typedHandler(eventData); } } }}Step 3: Usage in an application.
public class Also in library
Dependency Injection Containers in Rust: Overview and Implementation Using "dicy" as an Example
MapStruct for Java: Complete Guide with Examples
Working with Images in Swift: resize, crop (iOS/macOS)
JWT Tokens in C++: Implementation and Verification with jwt-cpp
Entity Framework Core for C# (.NET): Complete Guide with Examples