In this article, you will learn about the Observer Design Pattern in .NET C# with some enhancements.
The Observer Design Pattern is one of the most important and commonly used design patterns.
First, let’s check the formal definition of the Observer Design Pattern.
As per
The observer design pattern enables a subscriber to register with and receive notifications from a provider. It is suitable for any scenario that requires push-based notification. The pattern defines a provider (also known as a subject or an observable) and zero, one, or more observers. Observers register with the provider, and whenever a predefined condition, event, or state change occurs, the provider automatically notifies all observers by calling one of their methods. In this method call, the provider can also provide current state information to observers. In .NET, the observer design pattern is applied by implementing the generic System.IObservable<T> and System.IObserver<T> interfaces. The generic type parameter represents the type that provides notification information.
As we now know, the Observer Design Pattern formulates the relation between the Observable and Observer modules. What makes the Observer Design Pattern unique is that using it you can achieve this without having a tightly coupled relation.
Analyzing the way the pattern works, you would find the following:
These are the abstractions used to implement the Observer Design Pattern in .NET C#.
This is a Covariant interface representing any Observable. If you want to know more about Variance in .NET, you can check the article
Members defined in this interface are:
public IDisposable Subscribe (IObserver<out T> observer);
The Subscribe
method should be called to inform the Observable that some Observer is interested into its stream of information.
The Subscribe
method returns an object which implements the IDisposable
interface. This object could then be used by the Observer to unsubscribe from the stream of information provided by the Observable. Once this is done, the Observer would not be notified about any updates to the stream of information.
This is a Contravariant interface representing any Observer. If you want to know more about Variance in .NET, you can check the article
Members defined in this interface are:
public void OnCompleted ();
public void OnError (Exception error);
public void OnNext (T value);
The OnCompleted
method should be called by the Observable to inform the Observer that the stream of information is completed and the Observer should not expect any more information.
The OnError
method should be called by the Observable to inform the Observer that an error has occurred.
The OnNext
method should be called by the Observable to inform the Observer that a new piece of info is ready and is being added to the stream.
Now, let’s see how Microsoft recommends implementing the Observer Design Pattern in C#. Later, I will show you some minor enhancements I implemented myself.
We will build a simple Weather Forecast Console Application. In this application, we will have WeatherForecast module (Observable, Provider, Subject) and WeatherForecastObserver module (Observer).
So, let’s begin looking into the implementation.
namespace Observable
{
public class WeatherInfo
{
internal WeatherInfo(double temperature)
{
Temperature = temperature;
}
public double Temperature { get; }
}
}
This is the entity representing the piece of information to be flowing in the information stream.
using System;
using System.Collections.Generic;
namespace Observable
{
public class WeatherForecast : IObservable<WeatherInfo>
{
private readonly List<IObserver<WeatherInfo>> m_Observers;
private readonly List<WeatherInfo> m_WeatherInfoList;
public WeatherForecast()
{
m_Observers = new List<IObserver<WeatherInfo>>();
m_WeatherInfoList = new List<WeatherInfo>();
}
public IDisposable Subscribe(IObserver<WeatherInfo> observer)
{
if (!m_Observers.Contains(observer))
{
m_Observers.Add(observer);
foreach (var item in m_WeatherInfoList)
{
observer.OnNext(item);
}
}
return new WeatherForecastUnsubscriber(m_Observers, observer);
}
public void RegisterWeatherInfo(WeatherInfo weatherInfo)
{
m_WeatherInfoList.Add(weatherInfo);
foreach (var observer in m_Observers)
{
observer.OnNext(weatherInfo);
}
}
public void ClearWeatherInfo()
{
m_WeatherInfoList.Clear();
}
}
}
What we can notice here:
WeatherForecast
class is implementing IObservable<WeatherInfo>
.Subscribe
method, we check if the passed in Observer was already registered before or not. If not, we add it to the local m_Observers
observers list. Then, we loop on all the WeatherInfo
entries we have in the local m_WeatherInfoList
list one by one and inform the Observer about it by calling the OnNext
method of the Observer.WeatherForecastUnsubscriber
class to be used by the Observer for unsubscribing from the information stream.RegisterWeatherInfo
method is defined so that the main module can register new WeatherInfo
. In the real world, this could be replaced by an internal scheduled API call or a listener to a SignalR Hub or something else that would act as a source of information.
using System;
using System.Collections.Generic;
namespace Observable
{
public class Unsubscriber<T> : IDisposable
{
private readonly List<IObserver<T>> m_Observers;
private readonly IObserver<T> m_Observer;
private bool m_IsDisposed;
public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
{
m_Observers = observers;
m_Observer = observer;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (m_IsDisposed) return;
if (disposing && m_Observers.Contains(m_Observer))
{
m_Observers.Remove(m_Observer);
}
m_IsDisposed = true;
}
~Unsubscriber()
{
Dispose(false);
}
}
}
What we can notice here:
IDisposable
by applying the Disposable Design Pattern.
using System;
using System.Collections.Generic;
namespace Observable
{
public class WeatherForecastUnsubscriber : Unsubscriber<WeatherInfo>
{
public WeatherForecastUnsubscriber(
List<IObserver<WeatherInfo>> observers,
IObserver<WeatherInfo> observer) : base(observers, observer)
{
}
}
}
What we can notice here:
Unsubscriber<T>
class.
using System;
namespace Observable
{
public class WeatherForecastObserver : IObserver<WeatherInfo>
{
private IDisposable m_Unsubscriber;
public virtual void Subscribe(WeatherForecast provider)
{
m_Unsubscriber = provider.Subscribe(this);
}
public virtual void Unsubscribe()
{
m_Unsubscriber.Dispose();
}
public void OnCompleted()
{
Console.WriteLine("Completed");
}
public void OnError(Exception error)
{
Console.WriteLine("Error");
}
public void OnNext(WeatherInfo value)
{
Console.WriteLine($"Temperature: {value.Temperature}");
}
}
}
What we can notice here:
WeatherForecastObserver
class is implementing IObserver<WeatherInfo>
.OnNext
method, we are writing the temperature to the console.OnCompleted
method, we are writing “Completed” to the console.OnError
method, we are writing “Error” to the console.void Subscribe(WeatherForecast provider)
method to allow the main module to trigger the registration process. The un-subscriber object returned is saved internally to be used in case of unsubscribing.void Unsubscribe()
method is defined and it makes use of the internally saved un-subscriber object.
using System;
namespace Observable
{
class Program
{
static void Main(string[] args)
{
var provider = new WeatherForecast();
provider.RegisterWeatherInfo(new WeatherInfo(1));
provider.RegisterWeatherInfo(new WeatherInfo(2));
provider.RegisterWeatherInfo(new WeatherInfo(3));
var observer = new WeatherForecastObserver();
observer.Subscribe(provider);
provider.RegisterWeatherInfo(new WeatherInfo(4));
provider.RegisterWeatherInfo(new WeatherInfo(5));
observer.Unsubscribe();
provider.RegisterWeatherInfo(new WeatherInfo(6));
observer.Subscribe(provider);
provider.RegisterWeatherInfo(new WeatherInfo(7));
Console.ReadLine();
}
}
}
What we can notice here:
When I checked Microsoft’s implementation, I found some concerns. Therefore, I decided to do some minor changes.
using System;
using System.Collections.Generic;
namespace ExtendedObservable
{
public interface IExtendedObservable<out T> : IObservable<T>
{
IReadOnlyCollection<T> Snapshot { get; }
IDisposable Subscribe(IObserver<T> observer, bool withHistory);
}
}
What we can notice here:
IExtendedObservable<out T>
interface extends the IObservable<T>
interface.IReadOnlyCollection<T> Snapshot
property to allow other modules to get an instant list of already existing info entries without having to subscribe.IDisposable Subscribe(IObserver<T> observer, bool withHistory)
method with an extra bool withHistory
parameter so that the Observer can decide if it wants to get notified about the already existing info entries or not at the moment of subscribing.
using System;
namespace ExtendedObservable
{
public class Unsubscriber : IDisposable
{
private readonly Action m_UnsubscribeAction;
private bool m_IsDisposed;
public Unsubscriber(Action unsubscribeAction)
{
m_UnsubscribeAction = unsubscribeAction;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (m_IsDisposed) return;
if (disposing)
{
m_UnsubscribeAction();
}
m_IsDisposed = true;
}
~Unsubscriber()
{
Dispose(false);
}
}
}
What we can notice here:
Unsubscriber
class is not generic.
using System;
using System.Collections.Generic;
namespace ExtendedObservable
{
public class WeatherForecastUnsubscriber : Unsubscriber
{
public WeatherForecastUnsubscriber(
Action unsubscribeAction) : base(unsubscribeAction)
{
}
}
}
What we can notice here:
<T>
part from Unsubscriber<T>
.Action
to be called in case of disposing.
using System;
using System.Collections.Generic;
namespace ExtendedObservable
{
public class WeatherForecast : IExtendedObservable<WeatherInfo>
{
private readonly List<IObserver<WeatherInfo>> m_Observers;
private readonly List<WeatherInfo> m_WeatherInfoList;
public WeatherForecast()
{
m_Observers = new List<IObserver<WeatherInfo>>();
m_WeatherInfoList = new List<WeatherInfo>();
}
public IReadOnlyCollection<WeatherInfo> Snapshot => m_WeatherInfoList;
public IDisposable Subscribe(IObserver<WeatherInfo> observer)
{
return Subscribe(observer, false);
}
public IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
{
if (!m_Observers.Contains(observer))
{
m_Observers.Add(observer);
if (withHistory)
{
foreach (var item in m_WeatherInfoList)
{
observer.OnNext(item);
}
}
}
return new WeatherForecastUnsubscriber(
() =>
{
if (m_Observers.Contains(observer))
{
m_Observers.Remove(observer);
}
});
}
public void RegisterWeatherInfo(WeatherInfo weatherInfo)
{
m_WeatherInfoList.Add(weatherInfo);
foreach (var observer in m_Observers)
{
observer.OnNext(weatherInfo);
}
}
public void ClearWeatherInfo()
{
m_WeatherInfoList.Clear();
}
}
}
What we can notice here:
IReadOnlyCollection<WeatherInfo> Snapshot
property which returns the internal m_WeatherInfoList
list but as IReadOnlyCollection
.IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
method which makes use of the withHistory
parameter.
using System;
namespace ExtendedObservable
{
public class WeatherForecastObserver : IObserver<WeatherInfo>
{
private IDisposable m_Unsubscriber;
public virtual void Subscribe(WeatherForecast provider)
{
m_Unsubscriber = provider.Subscribe(this, true);
}
public virtual void Unsubscribe()
{
m_Unsubscriber.Dispose();
}
public void OnCompleted()
{
Console.WriteLine("Completed");
}
public void OnError(Exception error)
{
Console.WriteLine("Error");
}
public void OnNext(WeatherInfo value)
{
Console.WriteLine($"Temperature: {value.Temperature}");
}
}
}
What we can notice here is that it is almost the same except for Subscribe(WeatherForecast provider)
which now decides if it should Subscribe
with history or not.
using System;
namespace ExtendedObservable
{
class Program
{
static void Main(string[] args)
{
var provider = new WeatherForecast();
provider.RegisterWeatherInfo(new WeatherInfo(1));
provider.RegisterWeatherInfo(new WeatherInfo(2));
provider.RegisterWeatherInfo(new WeatherInfo(3));
var observer = new WeatherForecastObserver();
observer.Subscribe(provider);
provider.RegisterWeatherInfo(new WeatherInfo(4));
provider.RegisterWeatherInfo(new WeatherInfo(5));
observer.Unsubscribe();
provider.RegisterWeatherInfo(new WeatherInfo(6));
observer.Subscribe(provider);
provider.RegisterWeatherInfo(new WeatherInfo(7));
Console.ReadLine();
}
}
}
It is the same as before.
Now, you know the basics of the Observer Design Pattern in .NET C#. However, this is not the end of the story.
There are libraries built on top of IObservable<T>
and IObserver<T>
interfaces providing more cool features and capabilities which you might find useful.
On of these libraries is the
Therefore, I encourage you to explore these libraries and give them a try. I am sure you would like some of them.
Also published here.