Understanding the Observer Pattern in C# with IObservable and IObserver

posted 3 min read

Introduction

The Observer Pattern is a behavioral design pattern that allows an object (the subject) to notify multiple other objects (observers) about state changes. It is commonly used in event-driven programming, messaging systems, and real-time notifications.

C# provides a built-in way to implement this pattern using the IObservable and IObserver interfaces. Unlike traditional event handlers, IObservable offers better flexibility, structured error handling, and built-in subscription management.

Implementation of the Observer Pattern

The following example is based on the ObserverPattern repository by me.

using System;
using System.Collections.Generic;

namespace ObserverPattern
{
    // Observable: Order Processor
    public class OrderProcessor : IObservable<string>
    {
        private List<IObserver<string>> observers = new();

        public IDisposable Subscribe(IObserver<string> observer)
        {
            if (!observers.Contains(observer))
                observers.Add(observer);
            return new Unsubscriber(observers, observer);
        }

        public void ProcessOrder(int orderId)
        {
            Notify($"Order {orderId} is Processing");
            Notify($"Order {orderId} is Shipped");
            Notify($"Order {orderId} is Delivered");
        }

        private void Notify(string message)
        {
            foreach (var observer in observers)
                observer.OnNext(message);
        }

        private class Unsubscriber : IDisposable
        {
            private List<IObserver<string>> _observers;
            private IObserver<string> _observer;

            public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer)
            {
                _observers = observers;
                _observer = observer;
            }

            public void Dispose()
            {
                if (_observer != null && _observers.Contains(_observer))
                    _observers.Remove(_observer);
            }
        }
    }

    // Observer: Console Logger
    public class ConsoleLogger : IObserver<string>
    {
        public void OnNext(string value) => Console.WriteLine($"[Console] {value}");
        public void OnError(Exception error) => Console.WriteLine("[Console] Error occurred.");
        public void OnCompleted() => Console.WriteLine("[Console] No more updates.");
    }

    // Observer: File Logger
    public class FileLogger : IObserver<string>
    {
        private readonly string filePath = "log.txt";

        public void OnNext(string value)
        {
            using StreamWriter writer = new(filePath, append: true);
            writer.WriteLine(value);
        }

        public void OnError(Exception error)
        {
            using StreamWriter writer = new(filePath, append: true);
            writer.WriteLine("[File] Error occurred.");
        }

        public void OnCompleted()
        {
            using StreamWriter writer = new(filePath, append: true);
            writer.WriteLine("[File] No more updates.");
        }
    }

    // Main Program
    class Program
    {
        static void Main()
        {
            var orderProcessor = new OrderProcessor();
            var consoleLogger = new ConsoleLogger();
            var fileLogger = new FileLogger();

            using var consoleSubscription = orderProcessor.Subscribe(consoleLogger);
            using var fileSubscription = orderProcessor.Subscribe(fileLogger);

            orderProcessor.ProcessOrder(101);
        }
    }
}

1.OrderProcessor (Observable)

-Implements IObservable<string>    
-Notifies observers of order status changes    
-Allows observers to subscribe and unsubscribe dynamically

2.Observers (ConsoleLogger & FileLogger)

-Implement IObserver
-ConsoleLogger logs messages to the console
-FileLogger writes updates to a log file

3.Subscription Management

-Uses IDisposable to allow safe unsubscription
-Observers receive updates dynamically

Why Not Use Event Handlers (event and delegate)?

Benefits of Using IObservable and IObserver

✅ Better Resource Management: Unsubscribing is handled via IDisposable, reducing memory leaks.

✅ Built-in Error Handling: The OnError(Exception) method provides structured error handling.

✅ Completion Notification: OnCompleted() informs observers when no more updates will be sent.

✅ Supports Asynchronous & Reactive Programming: Works seamlessly with Reactive Extensions (Rx.NET).

✅ Scales Well for Complex Scenarios: Ideal for event-driven architectures where multiple observers must react to state changes dynamically.

Real-World Use Cases

  • Event-driven programming (e.g., UI events, logging systems)

  • Stock market applications (e.g., notifying traders about price

    changes)

  • Messaging and notification systems (e.g., email/SMS updates)

  • Reactive programming (e.g., Rx.NET, streaming APIs)

  • IoT (Internet of Things) (e.g., sensor data updates)

Conclusion

The Observer Pattern is an essential design pattern for event-driven applications. While event handlers (event and delegate) are useful for simple event propagation, IObservable and IObserver provide a more structured and flexible approach to managing subscriptions, error handling, and completion notifications.

By leveraging this pattern, developers can build scalable, reactive, and event-driven applications that efficiently manage state changes across multiple subscribers.

References

GitHub Repository:

        <a href="https://github.com/stevsharp/ObserverPattern" target="_blank">ObserverPattern</a>
        

Microsoft Docs:

Reactive Extensions (Rx.NET) Official:

Book Reference :

Design Patterns: Elements of Reusable Object-Oriented Software

If you read this far, tweet to the author to show them you care. Tweet a Thanks
nice one again Spyros, I'm curious, though—how does the performance of IObservable compare to standard event handlers when dealing with a high volume of events?
Thanks for your comments! Reactive Extensions (Rx) offer better resource management by handling unsubscription via IDisposable, which helps prevent memory leaks and ensures efficient cleanup of unused subscriptions.

There is also a great post from Steve Smith regarding event and why we shoudl avoid.
https://ardalis.com/avoid-using-csharp-events-in-aspnetcore-apps/

More Posts

Understanding Equality in C# with IEquatable, Not Equals and ==

Spyros - Feb 6

The State Pattern

Spyros - Jan 17

Modern Use OF Telnet with an example In dotnet,The Rise and Fall of Telnet: A Network Protocol's Journey

Moses Korir - Mar 12

When NOT to Use AsSplitQuery() in EF.core

Spyros - Feb 26

Suitable DB Choice in USSD Application

Moses Korir - Mar 12
chevron_left