Implement Observer Pattern in C# with Events and Rx
This article provides a concise guide to implementing the Observer
design pattern in C# using both native language events and Reactive
Extensions (Rx). You will learn how the built-in event
keyword provides a straightforward publish-subscribe mechanism, how
System.Reactive (IObservable<T> and
IObserver<T>) elevates event handling with powerful
LINQ-style composition, and how to choose the right approach for your
application.
Implementing the Observer Pattern Using C# Events
C# has built-in language support for the Observer pattern through
delegates and events. The publisher (Subject) exposes an
event, and subscribers (Observers) attach event handlers to
receive notifications.
Implementation
using System;
// 1. Define custom event arguments
public class PriceChangedEventArgs : EventArgs
{
public decimal NewPrice { get; }
public PriceChangedEventArgs(decimal newPrice)
{
NewPrice = newPrice;
}
}
// 2. The Subject (Publisher)
public class StockTicker
{
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
public void UpdatePrice(decimal newPrice)
{
// Notify all registered observers safely
PriceChanged?.Invoke(this, new PriceChangedEventArgs(newPrice));
}
}
// 3. The Observers (Subscribers)
public class Program
{
public static void Main()
{
var ticker = new StockTicker();
// Subscribe using a lambda or method group
ticker.PriceChanged += (sender, args) =>
{
Console.WriteLine($"Observer 1: Price changed to {args.NewPrice:C}");
};
ticker.PriceChanged += LogToConsole;
// Trigger updates
ticker.UpdatePrice(120.50m);
ticker.UpdatePrice(122.00m);
// Unsubscribe to avoid memory leaks
ticker.PriceChanged -= LogToConsole;
}
private static void LogToConsole(object? sender, PriceChangedEventArgs e)
{
Console.WriteLine($"Observer 2: Logged price {e.NewPrice:C}");
}
}Events are straightforward, require no external dependencies, and integrate directly with the .NET runtime. However, managing unsubscribing manually is necessary to prevent memory leaks, and complex operations such as throttling or combining multiple event sources require custom logic.
Implementing the Observer Pattern Using Reactive Extensions (Rx)
Reactive Extensions formalizes the Observer pattern via the
IObservable<T> and IObserver<T>
interfaces. Rx treats events as collections over time, allowing you to
filter, transform, and aggregate data streams using LINQ.
Install the required package via NuGet:
dotnet add package System.ReactiveImplementation
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
// 1. The Subject using Rx
public class StockTickerRx : IDisposable
{
// A Subject acts as both an Observer and an Observable
private readonly Subject<decimal> _priceSubject = new();
public IObservable<decimal> PriceStream => _priceSubject.AsObservable();
public void UpdatePrice(decimal newPrice)
{
_priceSubject.OnNext(newPrice);
}
public void Complete()
{
_priceSubject.OnCompleted();
}
public void Dispose()
{
_priceSubject.Dispose();
}
}
public class Program
{
public static void Main()
{
using var ticker = new StockTickerRx();
// 2. Subscribe directly to the stream
IDisposable subscription1 = ticker.PriceStream.Subscribe(
price => Console.WriteLine($"Observer 1: Price updated to {price:C}"),
ex => Console.WriteLine($"Error: {ex.Message}"),
() => Console.WriteLine("Stream completed.")
);
// 3. Transform and filter streams using Rx LINQ operators
IDisposable subscription2 = ticker.PriceStream
.Where(price => price > 100m)
.DistinctUntilChanged()
.Subscribe(price => Console.WriteLine($"Filtered Observer: High price {price:C}"));
ticker.UpdatePrice(95.00m);
ticker.UpdatePrice(105.00m);
ticker.UpdatePrice(105.00m); // Ignored by subscription2 due to DistinctUntilChanged
ticker.UpdatePrice(110.00m);
// Clean up subscriptions
subscription1.Dispose();
subscription2.Dispose();
}
}Rx handles lifetime management cleanly through
IDisposable. Disposing of the subscription token
automatically detaches the observer from the observable stream.
Choosing Between Events and Reactive Extensions
- Use C# Events when:
- Implementing standard UI event handling (such as WPF, WinForms, or MAUI).
- Creating lightweight notification systems within a single assembly.
- The application does not require asynchronous stream composition, debouncing, or complex filtering.
- Use Reactive Extensions (Rx) when:
- Working with asynchronous data streams that require querying, throttling, merging, or windowing.
- You need built-in handling for completion (
OnCompleted) and exceptions (OnError). - You want declarative subscription lifecycle management via
IDisposable.