Observer Pattern with Events and Delegates in C
The Observer pattern is a behavioral design pattern that defines a
one-to-many dependency between objects, allowing one object (the
subject) to notify multiple dependents (observers) when its state
changes. In C#, the language provides first-class support for this
pattern natively through delegates and events, eliminating the need to
manually implement boilerplate subject and observer interfaces. This
guide demonstrates how to build an idiomatic, type-safe implementation
of the Observer pattern using C#'s built-in event keyword
and EventHandler<T> delegate.
Core Concepts
In traditional object-oriented implementations of the Observer
pattern, you define an ISubject interface with
Attach(), Detach(), and Notify()
methods, alongside an IObserver interface with an
Update() method.
C# abstracts these responsibilities using:
- Delegates: Define the method signature for callbacks (acting as the abstract observer contract).
- Events: Encapsulate delegates on the publisher
(subject), restricting subscribers to only register (
+=) or unregister (-=) handlers. EventArgs: Encapsulates the state data passed from the publisher to the subscribers.
Step-by-Step Implementation
1. Define Custom Event Arguments
Create a class that inherits from EventArgs to carry the
state change information to subscribers.
using System;
public class OrderStatusChangedEventArgs : EventArgs
{
public int OrderId { get; }
public string NewStatus { get; }
public OrderStatusChangedEventArgs(int orderId, string newStatus)
{
OrderId = orderId;
NewStatus = newStatus;
}
}2. Implement the Subject (Publisher)
The subject exposes an event using
EventHandler<TEventArgs>. It triggers the event using
thread-safe null-conditional invocation (?.Invoke()).
public class Order
{
public int Id { get; }
private string _status;
// The event acts as the subject's notification mechanism
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
public Order(int id, string initialStatus)
{
Id = id;
_status = initialStatus;
}
public void UpdateStatus(string newStatus)
{
if (_status != newStatus)
{
_status = newStatus;
OnStatusChanged(new OrderStatusChangedEventArgs(Id, newStatus));
}
}
// Protected virtual method enables derived classes to handle or override the event
protected virtual void OnStatusChanged(OrderStatusChangedEventArgs e)
{
StatusChanged?.Invoke(this, e);
}
}3. Implement the Observers (Subscribers)
Observers define handler methods matching the signature
void Handler(object? sender, TEventArgs e) and attach them
to the subject's event.
public class EmailNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
{
Console.WriteLine($"[Email] Order #{e.OrderId} updated: {e.NewStatus}");
}
}
public class SmsNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
{
Console.WriteLine($"[SMS] Order #{e.OrderId} updated: {e.NewStatus}");
}
}4. Wire the Subject and Observers Together
Register handlers using the += operator and trigger
notifications by altering the subject's state.
public class Program
{
public static void Main()
{
var order = new Order(101, "Pending");
var emailNotifier = new EmailNotifier();
var smsNotifier = new SmsNotifier();
// Subscribe to the event
order.StatusChanged += emailNotifier.OnOrderStatusChanged;
order.StatusChanged += smsNotifier.OnOrderStatusChanged;
// Triggers notifications to both observers
order.UpdateStatus("Shipped");
// Unsubscribe an observer
order.StatusChanged -= smsNotifier.OnOrderStatusChanged;
// Triggers notification only to the email notifier
order.UpdateStatus("Delivered");
}
}Best Practices
- Prevent Memory Leaks: An event holds a strong
reference to its subscriber objects. If a publisher has a longer
lifespan than a subscriber, always unregister the handler using
-=or implementIDisposableon the subscriber to avoid keeping objects alive in memory. - Use Standard Signatures: Stick to
EventHandler<TEventArgs>rather than creating custom delegate types. This adheres to .NET framework design guidelines and ensures consistency across libraries. - Thread Safety: Invoking events via
StatusChanged?.Invoke(this, e)copies the delegate reference before evaluation, ensuring thread safety if a subscriber unregisters on another thread just before invocation.