Observer Design Pattern Best Practices

In .NET, the observer design pattern is implemented as a set of interfaces. The System.IObservable<T> interface represents the data provider, which is also responsible for providing an IDisposable implementation that lets observers unsubscribe from notifications. The System.IObserver<T> interface represents the observer. This topic describes the best practices that developers should follow when implementing the observer design pattern using these interfaces.

Threading

Typically, a provider implements the IObservable<T>.Subscribe method by adding a particular observer to a subscriber list that is represented by some collection object, and it implements the IDisposable.Dispose method by removing a particular observer from the subscriber list. An observer can call these methods at any time. In addition, because the provider/observer contract does not specify who is responsible for unsubscribing after the IObserver<T>.OnCompleted callback method, the provider and observer may both try to remove the same member from the list. Because of this possibility, both the Subscribe and Dispose methods should be thread-safe. Typically, this involves using a concurrent collection or a lock. Implementations that are not thread-safe should explicitly document that they are not.

Any additional guarantees have to be specified in a layer on top of the provider/observer contract. Implementers should clearly call out when they impose additional requirements to avoid user confusion about the observer contract.

Handling Exceptions

Because of the loose coupling between a data provider and an observer, exceptions in the observer design pattern are intended to be informational. This affects how providers and observers handle exceptions in the observer design pattern.

The Provider—Calling the OnError Method

The OnError method is intended as an informational message to observers, much like the IObserver<T>.OnNext method. However, the OnNext method is designed to provide an observer with current or updated data, whereas the OnError method is designed to indicate that the provider is unable to provide valid data.

The provider should follow these best practices when handling exceptions and calling the OnError method:

  • The provider must handle its own exceptions if it has any specific requirements.

  • The provider should not expect or require that observers handle exceptions in any particular way.

  • The provider should call the OnError method when it handles an exception that compromises its ability to provide updates. Information on such exceptions can be passed to the observer. In other cases, there is no need to notify observers of an exception.

Once the provider calls the OnError or IObserver<T>.OnCompleted method, there should be no further notifications, and the provider can unsubscribe its observers. However, the observers can also unsubscribe themselves at any time, including both before and after they receive an OnError or IObserver<T>.OnCompleted notification. The observer design pattern does not dictate whether the provider or the observer is responsible for unsubscribing; therefore, there is a possibility that both may attempt to unsubscribe. Typically, when observers unsubscribe, they are removed from a subscribers collection. In a single-threaded application, the IDisposable.Dispose implementation should ensure that an object reference is valid and that the object is a member of the subscribers collection before attempting to remove it. In a multithreaded application, a thread-safe collection object, such as a System.Collections.Concurrent.BlockingCollection<T> object, should be used.

The Observer—Implementing the OnError Method

When an observer receives an error notification from a provider, the observer should treat the exception as informational and should not be required to take any particular action.

The observer should follow these best practices when responding to an OnError method call from a provider:

  • The observer should not throw exceptions from its interface implementations, such as OnNext or OnError. However, if the observer does throw exceptions, it should expect these exceptions to go unhandled.

  • To preserve the call stack, an observer that wishes to throw an Exception object that was passed to its OnError method should wrap the exception before throwing it. A standard exception object should be used for this purpose.

Additional Best Practices

Attempting to unregister in the IObservable<T>.Subscribe method may result in a null reference. Therefore, we recommend that you avoid this practice.

Although it is possible to attach an observer to multiple providers, the recommended pattern is to attach an IObserver<T> instance to only one IObservable<T> instance.

See also