In this article, you will learn about the Observer Design Pattern in .NET C# with some enhancements.
Observer Design Pattern Definition
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.
So, from the definition above, we can understand the following:
- We have two parties or modules.
- The module which has some stream of information to provide. This module is called Provider (as it provides information), or Subject (as it subjects information to outside world), or Observable (as it could be observed by outside world).
- The module which is interested into a stream of information coming from somewhere else. This module is called Observer (as it observes information).
Advantages of Observer Design Pattern
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:
- The Observable knows the minimal information needed about the Observer.
- The Observer knows the minimal information needed about the Observable.
- Even the mutual knowledge is achieved through abstractions, not concrete implementations.
- At the end, both modules can do their job, and only their job.
Abstractions Used
These are the abstractions used to implement the Observer Design Pattern in .NET C#.
IObservable<out T>
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.
IObserver<in T>
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.
Microsoft’s Implementation
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.
WeatherInfo
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.
WeatherForecast
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:
- The
WeatherForecast
class is implementingIObservable<WeatherInfo>
. - In the implementation of the
Subscribe
method, we check if the passed in Observer was already registered before or not. If not, we add it to the localm_Observers
observers list. Then, we loop on all theWeatherInfo
entries we have in the localm_WeatherInfoList
list one by one and inform the Observer about it by calling theOnNext
method of the Observer. - Finally, we return a new instance of
WeatherForecastUnsubscriber
class to be used by the Observer for unsubscribing from the information stream. - The
RegisterWeatherInfo
method is defined so that the main module can register newWeatherInfo
. 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.
Unsubscriber<T>
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:
- This is a base class for any Un-subscriber.
- It implements
IDisposable
by applying the Disposable Design Pattern. - Through the constructor, it takes in the full list of Observers and the Observer it is created for.
- While disposing, it checks if the Observer already exists into the full list of Observers. If yes, it removes it from the list.
WeatherForecastUnsubscriber
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:
- This is inheriting from
Unsubscriber<T>
class. - No special handling is happening.
WeatherForecastObserver
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:
- The
WeatherForecastObserver
class is implementingIObserver<WeatherInfo>
. - On the
OnNext
method, we are writing the temperature to the console. - On the
OnCompleted
method, we are writing “Completed” to the console. - On the
OnError
method, we are writing “Error” to the console. - We defined
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. - Using the same concept, the
void Unsubscribe()
method is defined and it makes use of the internally saved un-subscriber object.
Program
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:
- We created an instance of the provider.
- Then registered 3 pieces of info.
- Up to this moment, nothing should be logged to the console as no observers are defined.
- Then created an instance of the observer.
- Then subscribed the observer to the stream.
- At this moment, we should find 3 logged temperatures in the console. This is because when the observer subscribes, it gets notified about the already existing information and in our case, they are 3 pieces of information.
- Then we register 2 pieces of info.
- So, we get 2 more messages logged to the console.
- Then we unsubscribe.
- Then we register 1 piece of info.
- However, this piece of info would not be logged to the console as the observer had already unsubscribed.
- Then the observer subscribes again.
- Then we register 1 piece of info.
- So, this piece of info is logged to the console.
Finally, running this should end up with this result:
My Extended Implementation
When I checked Microsoft’s implementation, I found some concerns. Therefore, I decided to do some minor changes.
IExtendedObservable<out T>
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:
- The
IExtendedObservable<out T>
interface extends theIObservable<T>
interface. - It is Covariant. If you want to know more about this, you can check the article
Covariance and Contravariance in .NET C# . - We defined
IReadOnlyCollection<T> Snapshot
property to allow other modules to get an instant list of already existing info entries without having to subscribe. - We also defined
IDisposable Subscribe(IObserver<T> observer, bool withHistory)
method with an extrabool 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.
Unsubscriber
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:
- Now, the
Unsubscriber
class is not generic. - This is because it doesn’t need any more to know the type of the info entity.
- Instead of having access to the full list of Observers and the Observer it is created for, it just notifies the Observable when it is disposed and the Observable handles the deregistration process by itself.
- This way, it is doing less than before and it is only doing its job.
WeatherForecastUnsubscriber
using System;
using System.Collections.Generic;
namespace ExtendedObservable
{
public class WeatherForecastUnsubscriber : Unsubscriber
{
public WeatherForecastUnsubscriber(
Action unsubscribeAction) : base(unsubscribeAction)
{
}
}
}
What we can notice here:
- We removed the
<T>
part fromUnsubscriber<T>
. - And now the constructor takes in an
Action
to be called in case of disposing.
WeatherForecast
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:
- It is almost the same except for
IReadOnlyCollection<WeatherInfo> Snapshot
property which returns the internalm_WeatherInfoList
list but asIReadOnlyCollection
. - And
IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
method which makes use of thewithHistory
parameter.
WeatherForecastObserver
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.
Program
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.
Finally, running this should end up with the same result as before:
What’s Next
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.