Understanding the Observer Pattern in C# with IObservable and IObserver

Leader 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
0 votes
0 votes

More Posts

Understanding MediatR Assembly Registration in .NET

Moses Korir - Jul 4

Reliable Messaging in .NET: Domain Events and the Outbox Pattern with EF Core Interceptors

Spyros - Oct 13

Building a Pub/Sub System in .NET: MassTransit, Reactive Extensions, and BlockingCollection

Spyros - Aug 26

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

Spyros - Feb 6

The State Pattern

Spyros - Jan 17
chevron_left