paint-brush
An Essential Guide to Observer Design Pattern in .NET C#by@ahmedtarekhasan
3,303 reads
3,303 reads

An Essential Guide to Observer Design Pattern in .NET C#

by Ahmed Tarek HasanApril 10th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

In this article, you will learn about the Observer Design Pattern in.NET C# with some enhancements. 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. What makes the pattern unique is that using it you can achieve this without having a tightly coupled relation.
featured image - An Essential Guide to Observer Design Pattern in .NET C#
Ahmed Tarek Hasan HackerNoon profile picture

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 Microsoft’s documentation:


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:

  1. We have two parties or modules.
  2. 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).
  3. The module which is interested into a stream of information coming from somewhere else. This module is called Observer (as it observes information).

Photo by Den Harrson on Unsplash

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:

  1. The Observable knows the minimal information needed about the Observer.
  2. The Observer knows the minimal information needed about the Observable.
  3. Even the mutual knowledge is achieved through abstractions, not concrete implementations.
  4. At the end, both modules can do their job, and only their job.

Photo by Lucas Santos on Unsplash

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 Covariance and Contravariance in .NET C#.


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 Covariance and Contravariance in .NET C#.


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.


Photo by Tadas Sar on Unsplash

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:

  1. The WeatherForecast class is implementing IObservable<WeatherInfo>.
  2. 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 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.
  3. Finally, we return a new instance of WeatherForecastUnsubscriber class to be used by the Observer for unsubscribing from the information stream.
  4. The 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.



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:

  1. This is a base class for any Un-subscriber.
  2. It implements IDisposable by applying the Disposable Design Pattern.
  3. Through the constructor, it takes in the full list of Observers and the Observer it is created for.
  4. 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:

  1. This is inheriting from Unsubscriber<T> class.
  2. 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:

  1. The WeatherForecastObserver class is implementing IObserver<WeatherInfo>.
  2. On the OnNext method, we are writing the temperature to the console.
  3. On the OnCompleted method, we are writing “Completed” to the console.
  4. On the OnError method, we are writing “Error” to the console.
  5. 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.
  6. 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:

  1. We created an instance of the provider.
  2. Then registered 3 pieces of info.
  3. Up to this moment, nothing should be logged to the console as no observers are defined.
  4. Then created an instance of the observer.
  5. Then subscribed the observer to the stream.
  6. 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.
  7. Then we register 2 pieces of info.
  8. So, we get 2 more messages logged to the console.
  9. Then we unsubscribe.
  10. Then we register 1 piece of info.
  11. However, this piece of info would not be logged to the console as the observer had already unsubscribed.
  12. Then the observer subscribes again.
  13. Then we register 1 piece of info.
  14. So, this piece of info is logged to the console.


Finally, running this should end up with this result:


Image by Ahmed Tarek


Photo by Bruno Yamazaky on Unsplash

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:

  1. The IExtendedObservable<out T> interface extends the IObservable<T> interface.
  2. It is Covariant. If you want to know more about this, you can check the article Covariance and Contravariance in .NET C#.
  3. We defined IReadOnlyCollection<T> Snapshot property to allow other modules to get an instant list of already existing info entries without having to subscribe.
  4. We also defined 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.



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:

  1. Now, the Unsubscriber class is not generic.
  2. This is because it doesn’t need any more to know the type of the info entity.
  3. 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.
  4. 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:

  1. We removed the <T> part from Unsubscriber<T>.
  2. 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:

  1. It is almost the same except for IReadOnlyCollection<WeatherInfo> Snapshot property which returns the internal m_WeatherInfoList list but as IReadOnlyCollection.
  2. And IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) method which makes use of the withHistory 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:


Image by Ahmed Tarek


Photo by Emily Morter on Unsplash, adjusted by Ahmed Tarek

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 Reactive Extensions for .NET (Rx) library. It consists of a set of extension methods and LINQ standard sequence operators to support asynchronous programming.


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.