BehavioralC#verifiedVerified
Observer Pattern in C#
Defines a one-to-many dependency so that when one object changes state, all its dependents are notified and updated automatically.
How to Implement the Observer Pattern in C#
1Step 1: Define the observer interface
public interface IObserver<in T>
{
void Update(T data);
}2Step 2: Subject maintains a list of observers
public class Subject<T>
{
private readonly List<IObserver<T>> _observers = [];
public void Subscribe(IObserver<T> observer) =>
_observers.Add(observer);
public void Unsubscribe(IObserver<T> observer) =>
_observers.Remove(observer);3Step 3: Notify all observers when state changes
public void Notify(T data)
{
foreach (var observer in _observers)
observer.Update(data);
}
}4Step 4: Concrete observer
public class ConsoleLogger : IObserver<string>
{
public void Update(string data) =>
Console.WriteLine($"Log: {data}");
}
// Usage:
// var subject = new Subject<string>();
// subject.Subscribe(new ConsoleLogger());
// subject.Notify("Something happened");using Microsoft.Extensions.Logging;
// [step] Define typed event system with async support
public record EventEnvelope<T>(
string EventId, T Data, DateTime Timestamp,
string Source);
public interface IEventHandler<T>
{
Task HandleAsync(EventEnvelope<T> envelope,
CancellationToken ct = default);
}
// [step] Thread-safe event bus with weak references and filtering
public sealed class EventBus : IDisposable
{
private readonly Dictionary<Type, List<Delegate>> _handlers = [];
private readonly ReaderWriterLockSlim _lock = new();
private readonly ILogger<EventBus> _logger;
public EventBus(ILogger<EventBus> logger) => _logger = logger;
public IDisposable Subscribe<T>(IEventHandler<T> handler)
{
_lock.EnterWriteLock();
try
{
var key = typeof(T);
if (!_handlers.TryGetValue(key, out var list))
{
list = [];
_handlers[key] = list;
}
list.Add((Func<EventEnvelope<T>, CancellationToken, Task>)
handler.HandleAsync);
}
finally { _lock.ExitWriteLock(); }
_logger.LogDebug("Subscribed handler for {Type}", typeof(T).Name);
return new Subscription(() => Unsubscribe<T>(handler));
}
public IDisposable Subscribe<T>(
Func<EventEnvelope<T>, CancellationToken, Task> handler)
{
_lock.EnterWriteLock();
try
{
var key = typeof(T);
if (!_handlers.TryGetValue(key, out var list))
{
list = [];
_handlers[key] = list;
}
list.Add(handler);
}
finally { _lock.ExitWriteLock(); }
return new Subscription(() =>
{
_lock.EnterWriteLock();
try { _handlers[typeof(T)].Remove(handler); }
finally { _lock.ExitWriteLock(); }
});
}
// [step] Publish with parallel notification and error isolation
public async Task PublishAsync<T>(
T data, string source = "system",
CancellationToken ct = default)
{
var envelope = new EventEnvelope<T>(
Guid.NewGuid().ToString(), data, DateTime.UtcNow, source);
List<Delegate> snapshot;
_lock.EnterReadLock();
try
{
if (!_handlers.TryGetValue(typeof(T), out var list)) return;
snapshot = [.. list];
}
finally { _lock.ExitReadLock(); }
var tasks = snapshot.Select(async h =>
{
try
{
var fn = (Func<EventEnvelope<T>, CancellationToken, Task>)h;
await fn(envelope, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Handler failed for {Type}", typeof(T).Name);
}
});
await Task.WhenAll(tasks);
}
private void Unsubscribe<T>(IEventHandler<T> handler)
{
_lock.EnterWriteLock();
try
{
if (_handlers.TryGetValue(typeof(T), out var list))
list.RemoveAll(d => d.Target == handler);
}
finally { _lock.ExitWriteLock(); }
}
public void Dispose() => _lock.Dispose();
private sealed class Subscription(Action onDispose) : IDisposable
{
public void Dispose() => onDispose();
}
}Observer Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Observer Pattern in the Real World
“Think of a newspaper subscription service. The publisher (subject) doesn't know exactly who its subscribers (observers) are—it just maintains a list. When a new edition is printed, it delivers a copy to every subscriber on the list automatically. Subscribers can cancel at any time without the publisher needing to change anything.”