How to Implement Observer Pattern in C#: Step-by-Step Guide
Reacting to changes is at the heart of most real-world software. Whether a sensor pushes a new reading, a user updates their profile, or a background job finishes, something in the system needs to know about it. The observer pattern formalizes this "something changed, tell everyone who cares" relationship -- and C# gives you several ways to express it. If you want to implement observer pattern in C#, this guide walks you through three progressively powerful approaches.
The observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects. When the "subject" changes state, all registered "observers" are notified automatically. This decouples the thing that changes from the things that react, which keeps your code modular and easier to extend over time. If you're exploring design patterns more broadly, check out The Big List of Design Patterns for an overview of where the observer pattern fits in the bigger picture.
In this step-by-step guide, we'll build a weather monitoring system using the observer pattern. We'll start with a hand-rolled implementation using custom interfaces, refactor it to use C# events, and then rebuild it with .NET's built-in IObservable<T> and IObserver<T> interfaces. By the end, you'll understand the trade-offs of each approach and know how to pick the right one for your use case.
Prerequisites
Before diving in, make sure you're comfortable with these fundamentals:
- C# basics: Classes, interfaces, properties, and constructors. The code examples use standard object-oriented features throughout.
- Delegates and events: We'll use
EventHandler<T>and theeventkeyword in the C# events section. If you've subscribed to a button click event before, you have enough context. For async event scenarios, check out Async Event Handlers in C#. - Interfaces and composition: The observer pattern leans on composition -- subjects hold references to observers through an interface rather than concrete types. Understanding why you'd program to an interface will help this pattern click.
- .NET 8 or later: The code examples target modern C# syntax. Any recent .NET SDK will work.
Step 1: Define the Observer Interface
The first step to implement observer pattern in C# is creating a contract for observers. This interface tells every observer what information it will receive when the subject's state changes. By coding to an interface, the subject never needs to know the concrete types of the objects listening to it.
For our weather monitoring example, we'll define an IWeatherObserver that receives temperature, humidity, and pressure readings:
public interface IWeatherObserver
{
void Update(
double temperature,
double humidity,
double pressure);
}
The Update method is the notification callback. When the weather station records new data, it calls this method on every registered observer. Keeping the interface narrow -- a single method with clear parameters -- reduces the contract burden on each observer implementation. If you've worked with the strategy pattern, you'll recognize a similar emphasis on small, focused interfaces.
You could also define a data class to bundle these readings together, which we'll explore in later steps. For now, individual parameters keep things explicit and easy to follow.
Step 2: Define the Subject Interface
Next, we need a contract for the subject -- the object that holds state and notifies observers when it changes. The subject interface defines how observers subscribe, unsubscribe, and how notifications are triggered.
public interface IWeatherStation
{
void Subscribe(IWeatherObserver observer);
void Unsubscribe(IWeatherObserver observer);
void NotifyObservers();
}
Three methods are the standard minimum when you implement observer pattern in C#:
- Subscribe registers an observer so it receives future notifications. This is sometimes called
AttachorRegisterin other implementations. - Unsubscribe removes an observer so it stops receiving notifications. Forgetting to unsubscribe is one of the most common sources of memory leaks in event-driven code -- we'll address that directly in the common mistakes section.
- NotifyObservers iterates through all registered observers and calls their
Updatemethod. Some implementations make this private or protected, but exposing it on the interface gives us flexibility during testing.
This interface embodies inversion of control -- the subject doesn't call observers directly by their concrete type. Instead, it delegates through the IWeatherObserver abstraction, which means you can add new observer types without modifying the subject.
Step 3: Implement the Concrete Subject
With both interfaces defined, we can build the concrete subject. The WeatherStation class tracks weather readings internally and notifies all registered observers whenever its measurements are updated.
using System;
using System.Collections.Generic;
public class WeatherStation : IWeatherStation
{
private readonly List<IWeatherObserver> _observers;
private double _temperature;
private double _humidity;
private double _pressure;
public WeatherStation()
{
_observers = new List<IWeatherObserver>();
}
public void Subscribe(IWeatherObserver observer)
{
if (observer == null)
{
throw new ArgumentNullException(nameof(observer));
}
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
}
public void Unsubscribe(IWeatherObserver observer)
{
_observers.Remove(observer);
}
public void NotifyObservers()
{
foreach (var observer in _observers.ToArray())
{
observer.Update(
_temperature,
_humidity,
_pressure);
}
}
public void SetMeasurements(
double temperature,
double humidity,
double pressure)
{
_temperature = temperature;
_humidity = humidity;
_pressure = pressure;
NotifyObservers();
}
}
A few things to notice here:
- The
Subscribemethod checks for duplicates before adding an observer. This prevents the same observer from receiving multiple notifications for a single state change. - In
NotifyObservers, we iterate over_observers.ToArray()instead of iterating_observersdirectly. This creates a snapshot of the list, which protects againstInvalidOperationExceptionif an observer unsubscribes itself during notification -- a subtle but common bug we'll revisit in the common mistakes section. - The
SetMeasurementsmethod updates the internal state and immediately triggers notification. This "push" model sends data to observers proactively rather than waiting for them to request it.
Step 4: Implement Concrete Observers
Now let's create two concrete observer implementations to implement observer pattern in C# end to end. Each observer receives the same data but does something different with it.
Console Display Observer
This observer prints the current weather readings to the console:
using System;
public class ConsoleDisplayObserver : IWeatherObserver
{
private readonly string _displayName;
public ConsoleDisplayObserver(string displayName)
{
_displayName = displayName;
}
public void Update(
double temperature,
double humidity,
double pressure)
{
Console.WriteLine(
$"[{_displayName}] Weather Update: " +
$"Temp={temperature:F1}°C, " +
$"Humidity={humidity:F1}%, " +
$"Pressure={pressure:F1} hPa");
}
}
Alert Observer
This observer checks whether conditions exceed a threshold and raises an alert:
using System;
public class AlertObserver : IWeatherObserver
{
private readonly double _temperatureThreshold;
public AlertObserver(double temperatureThreshold)
{
_temperatureThreshold = temperatureThreshold;
}
public void Update(
double temperature,
double humidity,
double pressure)
{
if (temperature > _temperatureThreshold)
{
Console.WriteLine(
$"[ALERT] Temperature {temperature:F1}°C " +
$"exceeds threshold of " +
$"{_temperatureThreshold:F1}°C!");
}
}
}
Both observers implement the same IWeatherObserver interface, but their behavior is completely different. The console display always prints; the alert observer only prints when a condition is met. This demonstrates one of the observer pattern's key strengths -- you can add arbitrarily many listeners with different behaviors, and the subject never needs to change. This is composition at work, where each observer encapsulates its own reaction logic independently.
Step 5: Wire Up and Test
Let's bring everything together. We'll create the subject, register both observers, and push a few updates through the system:
using System;
var station = new WeatherStation();
var mainDisplay = new ConsoleDisplayObserver("Main Display");
var backupDisplay = new ConsoleDisplayObserver("Backup Display");
var heatAlert = new AlertObserver(temperatureThreshold: 35.0);
station.Subscribe(mainDisplay);
station.Subscribe(backupDisplay);
station.Subscribe(heatAlert);
Console.WriteLine("--- First reading ---");
station.SetMeasurements(28.5, 65.0, 1013.25);
Console.WriteLine();
Console.WriteLine("--- Second reading (hot!) ---");
station.SetMeasurements(37.2, 70.0, 1008.50);
Console.WriteLine();
Console.WriteLine("--- After unsubscribing backup ---");
station.Unsubscribe(backupDisplay);
station.SetMeasurements(22.0, 55.0, 1015.00);
Running this produces output like:
--- First reading ---
[Main Display] Weather Update: Temp=28.5°C, Humidity=65.0%, Pressure=1013.3 hPa
[Backup Display] Weather Update: Temp=28.5°C, Humidity=65.0%, Pressure=1013.3 hPa
--- Second reading (hot!) ---
[Main Display] Weather Update: Temp=37.2°C, Humidity=70.0%, Pressure=1008.5 hPa
[Backup Display] Weather Update: Temp=37.2°C, Humidity=70.0%, Pressure=1008.5 hPa
[ALERT] Temperature 37.2°C exceeds threshold of 35.0°C!
--- After unsubscribing backup ---
[Main Display] Weather Update: Temp=22.0°C, Humidity=55.0%, Pressure=1015.0 hPa
The backup display stops receiving updates after we unsubscribe it. The alert observer only fires when the temperature exceeds 35°C. This is the observer pattern working exactly as designed -- subjects push notifications, observers react independently, and the lifecycle of each subscription is managed explicitly.
Step 6: Implement with C# Events
C# has first-class language support for the observer pattern through delegates and events. The event keyword provides a built-in mechanism for subscription, unsubscription, and notification -- which means you can implement observer pattern in C# without writing your own Subscribe and Unsubscribe plumbing.
Let's refactor the weather station to use events:
using System;
public class WeatherChangedEventArgs : EventArgs
{
public double Temperature { get; }
public double Humidity { get; }
public double Pressure { get; }
public WeatherChangedEventArgs(
double temperature,
double humidity,
double pressure)
{
Temperature = temperature;
Humidity = humidity;
Pressure = pressure;
}
}
public class EventBasedWeatherStation
{
public event EventHandler<WeatherChangedEventArgs>?
WeatherChanged;
public void SetMeasurements(
double temperature,
double humidity,
double pressure)
{
WeatherChanged?.Invoke(
this,
new WeatherChangedEventArgs(
temperature,
humidity,
pressure));
}
}
Now observers subscribe using standard C# event syntax:
using System;
var station = new EventBasedWeatherStation();
station.WeatherChanged += (sender, e) =>
{
Console.WriteLine(
$"[Event Display] Temp={e.Temperature:F1}°C, " +
$"Humidity={e.Humidity:F1}%, " +
$"Pressure={e.Pressure:F1} hPa");
};
EventHandler<WeatherChangedEventArgs> alertHandler =
(sender, e) =>
{
if (e.Temperature > 35.0)
{
Console.WriteLine(
$"[Event ALERT] Temperature " +
$"{e.Temperature:F1}°C is too high!");
}
};
station.WeatherChanged += alertHandler;
Console.WriteLine("--- Event-based reading ---");
station.SetMeasurements(38.0, 72.0, 1005.0);
Console.WriteLine();
Console.WriteLine("--- After removing alert handler ---");
station.WeatherChanged -= alertHandler;
station.SetMeasurements(38.0, 72.0, 1005.0);
The event-based approach has several advantages over the hand-rolled version. The event keyword restricts external code from invoking the event directly -- only the owning class can raise it, which prevents accidental misuse. The += and -= operators provide clean subscription syntax that every C# developer already knows. And the compiler enforces delegate signature compatibility, catching type mismatches at compile time.
However, there's a trade-off. Because event handlers use delegates, if a subscriber is a long-lived object that holds a reference to a short-lived one, you can run into memory leaks. The subscriber keeps the handler alive, which keeps the handler's target alive. If you're concerned about this scenario, read about Weak Events in C# for strategies to mitigate the problem.
Step 7: Implement with IObservable<T> and IObserver<T>
.NET provides built-in interfaces specifically designed for the observer pattern: IObservable<T> and IObserver<T>. These live in the System namespace and add structure around subscription lifecycle management through IDisposable. Let's implement observer pattern in C# one more time using this approach.
First, define the data model and the observable subject:
using System;
using System.Collections.Generic;
public class WeatherData
{
public double Temperature { get; }
public double Humidity { get; }
public double Pressure { get; }
public WeatherData(
double temperature,
double humidity,
double pressure)
{
Temperature = temperature;
Humidity = humidity;
Pressure = pressure;
}
}
public class ObservableWeatherStation
: IObservable<WeatherData>
{
private readonly List<IObserver<WeatherData>> _observers;
public ObservableWeatherStation()
{
_observers = new List<IObserver<WeatherData>>();
}
public IDisposable Subscribe(
IObserver<WeatherData> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
return new Unsubscriber(_observers, observer);
}
public void SetMeasurements(
double temperature,
double humidity,
double pressure)
{
var data = new WeatherData(
temperature,
humidity,
pressure);
foreach (var observer in _observers.ToArray())
{
observer.OnNext(data);
}
}
public void EndTransmission()
{
foreach (var observer in _observers.ToArray())
{
observer.OnCompleted();
}
_observers.Clear();
}
private sealed class Unsubscriber : IDisposable
{
private readonly List<IObserver<WeatherData>>
_observers;
private readonly IObserver<WeatherData> _observer;
public Unsubscriber(
List<IObserver<WeatherData>> observers,
IObserver<WeatherData> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer))
{
_observers.Remove(_observer);
}
}
}
}
The key difference is the Subscribe method returns an IDisposable. This lets observers unsubscribe by disposing the token rather than needing a reference back to the subject. The nested Unsubscriber class captures the observer list and the specific observer, so calling Dispose() removes that observer cleanly.
Now create a concrete observer that implements IObserver<WeatherData>:
using System;
public class WeatherReporter : IObserver<WeatherData>
{
private readonly string _name;
private IDisposable? _unsubscriber;
public WeatherReporter(string name)
{
_name = name;
}
public void SubscribeTo(
IObservable<WeatherData> provider)
{
_unsubscriber = provider.Subscribe(this);
}
public void OnNext(WeatherData value)
{
Console.WriteLine(
$"[{_name}] Temp={value.Temperature:F1}°C, " +
$"Humidity={value.Humidity:F1}%, " +
$"Pressure={value.Pressure:F1} hPa");
}
public void OnError(Exception error)
{
Console.WriteLine(
$"[{_name}] Error: {error.Message}");
}
public void OnCompleted()
{
Console.WriteLine(
$"[{_name}] Weather station has " +
$"stopped transmitting.");
_unsubscriber?.Dispose();
}
}
Wire it up and test:
using System;
var station = new ObservableWeatherStation();
var reporter1 = new WeatherReporter("City Center");
var reporter2 = new WeatherReporter("Airport");
reporter1.SubscribeTo(station);
reporter2.SubscribeTo(station);
Console.WriteLine("--- IObservable reading ---");
station.SetMeasurements(25.0, 60.0, 1012.0);
Console.WriteLine();
Console.WriteLine("--- End transmission ---");
station.EndTransmission();
The IObservable<T> / IObserver<T> approach is more structured than both the hand-rolled and event-based implementations. It explicitly models three scenarios: data arrives (OnNext), an error occurs (OnError), and the stream ends (OnCompleted). The IDisposable return from Subscribe provides deterministic cleanup, which helps prevent the memory leaks that plague event-based systems. If you're building reactive or streaming architectures, this approach also aligns with libraries like System.Reactive (Rx.NET), which extend IObservable<T> with powerful LINQ-style operators.
Common Mistakes to Avoid
Even experienced developers hit these pitfalls when they first implement observer pattern in C#. Knowing them ahead of time saves debugging hours.
Memory leaks from forgotten subscriptions: This is the number one issue. If an observer subscribes to a subject but never unsubscribes, the subject holds a reference to the observer indefinitely. In long-running applications, this prevents the garbage collector from reclaiming the observer's memory. The event-based approach is particularly susceptible because += is easy to call and -= is easy to forget. The IDisposable pattern from IObservable<T> helps by making subscription cleanup explicit, and weak events offer another mitigation strategy.
Modifying the observer list during notification: If an observer unsubscribes itself (or subscribes a new observer) inside its Update callback, and you're iterating the observer list directly, you'll get an InvalidOperationException. The fix is straightforward -- iterate over a copy of the list using .ToArray() as shown in our examples above. This snapshot approach ensures the notification loop completes safely even if the list changes mid-iteration.
Thread safety: If multiple threads call SetMeasurements or Subscribe simultaneously, the internal List<T> can corrupt. For thread-safe scenarios, consider using ConcurrentBag<T> or wrapping access with a lock statement. The event-based approach inherits some thread safety from the way the CLR handles delegate invocation, but you still need to handle the null check pattern carefully -- use the null-conditional ?.Invoke() syntax rather than a separate null check followed by invocation.
Notifying in an undefined order: The List<T>-based implementations above notify observers in insertion order, but this is an implementation detail, not a guarantee. Do not write observers that depend on being notified before or after another observer. Each observer should be self-contained and independent.
Sending too much data: Pushing large payloads to every observer on every state change can cause performance problems. If only the temperature changed, some observers might not need humidity and pressure. Consider a more granular notification model -- such as property-specific events or a change-tracking approach -- when performance matters.
Frequently Asked Questions
What is the observer pattern and when should I use it in C#?
The observer pattern defines a one-to-many dependency so that when one object changes state, all dependents are notified automatically. You should implement observer pattern in C# whenever you need loose coupling between a data source and the components that react to changes. Common examples include UI data binding, event logging, real-time dashboards, and message broadcasting. If you find yourself writing code where one object directly calls methods on several other objects after a state change, that's a strong signal the observer pattern would clean things up.
Should I use custom interfaces, C# events, or IObservable<T>?
It depends on your scenario. Custom interfaces give you full control over the subscription API and work well in educational or simple use cases. C# events are idiomatic and familiar to every .NET developer -- use them for component-level notifications within an application. IObservable<T> / IObserver<T> is the right choice for streaming data, when you need deterministic cleanup via IDisposable, or when you plan to use Rx.NET for composing complex event pipelines. For most business applications, C# events are the pragmatic default.
How do I prevent memory leaks with the observer pattern?
Always unsubscribe observers when they are no longer needed. For C# events, pair every += with a corresponding -=, ideally in a Dispose method or finalizer. For IObservable<T>, hold onto the IDisposable returned by Subscribe and call Dispose() when the observer should stop listening. You can also explore weak events for scenarios where you can't guarantee explicit unsubscription.
How does the observer pattern relate to other design patterns?
The observer pattern complements several other design patterns. The strategy pattern lets you swap algorithms, while the observer pattern lets multiple objects react to changes. The decorator pattern adds behavior by wrapping objects, while the observer pattern adds behavior by registering listeners. You can combine patterns -- for example, using a decorator to add logging around an observer's notification handler.
Is the observer pattern the same as the publish-subscribe pattern?
They are closely related but not identical. In the classic observer pattern, subjects know their observers directly -- they hold references to them. In publish-subscribe (pub/sub), a message broker or event bus sits between publishers and subscribers, fully decoupling the two sides. Pub/sub is more flexible for distributed systems, while the observer pattern is simpler and works well within a single process.
Can I use the observer pattern with dependency injection in C#?
Yes. You can register observers in your IServiceCollection and resolve them when constructing the subject. One approach is to inject IEnumerable<IWeatherObserver> into the subject's constructor, which automatically resolves all registered observer implementations. This lets you add new observers by registering them in the DI container without modifying the subject class at all.
How do I handle errors thrown by observers?
If one observer throws an exception during notification, it can prevent subsequent observers from being notified. A defensive approach is to wrap each Update call in a try-catch block inside the notification loop and log the error without propagating it. The IObserver<T> interface addresses this explicitly through the OnError method, which provides a structured way to communicate errors back to the observer. Whichever approach you use, one misbehaving observer should never break the entire notification chain.

