DEV_NET_CORE
GET_STARTED
.NETModern C# patterns

Observer-Style Communication in C#

Overview

Observer-style communication is a design approach where one object publishes a notification and one or more other objects react to it without the publisher needing to know exactly who the subscribers are. In C#, this is commonly implemented with events, delegates, EventHandler<TEventArgs>, INotifyPropertyChanged, IObservable<T>, IObserver<T>, mediator-style messaging, or domain events.

The core idea is simple: instead of one component directly calling many other components, it exposes a notification mechanism. Interested components subscribe, and the publisher raises a notification when something important happens.

This matters because real applications often need loosely coupled communication. A button click should notify UI code. A view model should notify the UI when a property changes. A domain entity may record that an order was placed. A background service may publish progress updates. A cache may notify listeners when data changes. Observer-style communication helps separate the component that detects a change from the components that respond to that change.

For interviews, this topic is important because it tests both language knowledge and design judgment. A strong candidate should understand how C# events work, how the observer design pattern works, when to use IObservable<T>, how to avoid memory leaks caused by event subscriptions, and how observer-style communication compares with direct method calls, callbacks, mediator patterns, message queues, and pub/sub systems.

Core Concepts

What Observer-Style Communication Means

Observer-style communication is based on two roles:

  • Publisher, subject, provider, or observable: the object that owns some state or detects an event.
  • Subscriber, observer, listener, or handler: the object that wants to be notified when something happens.

The publisher does not need to know the concrete subscriber types. It only needs to provide a way for subscribers to register and unregister.

A simple real-world example is a notification system:

Code
public sealed class OrderService
{
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

    public void PlaceOrder(int orderId)
    {
        // Save order, validate business rules, update database, etc.

        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(orderId));
    }
}

public sealed class OrderPlacedEventArgs : EventArgs
{
    public OrderPlacedEventArgs(int orderId)
    {
        OrderId = orderId;
    }

    public int OrderId { get; }
}

A subscriber can react to the event:

Code
var orderService = new OrderService();

orderService.OrderPlaced += (sender, args) =>
{
    Console.WriteLine($"Order placed: {args.OrderId}");
};

orderService.PlaceOrder(123);

The OrderService does not know whether the subscriber logs to console, sends an email, updates a dashboard, or triggers another workflow.

Why This Pattern Is Useful

Observer-style communication is useful when one action should trigger multiple independent reactions.

Common examples include:

  • UI events such as button clicks, text changes, and form submissions.
  • View model updates through INotifyPropertyChanged.
  • Domain events such as OrderPlaced, PaymentCaptured, or UserRegistered.
  • Progress notifications from background tasks.
  • Cache invalidation notifications.
  • Real-time data updates.
  • Event-driven architecture inside an application.
  • Reactive streams using IObservable<T>.
  • Decoupling application services from side effects.

Without observer-style communication, a publisher often becomes tightly coupled to every component that needs to react:

Code
public void PlaceOrder(int orderId)
{
    SaveOrder(orderId);

    _emailService.SendOrderConfirmation(orderId);
    _auditService.LogOrderPlaced(orderId);
    _inventoryService.ReserveStock(orderId);
    _notificationService.PushOrderUpdate(orderId);
}

This can be acceptable for simple workflows, but it becomes harder to maintain when many independent actions must occur after the same event.

With observer-style communication, the publisher can focus on publishing the event, while subscribers own their own reactions.

C# Events and Delegates

In C#, events are the most common built-in mechanism for observer-style communication.

An event is based on a delegate. A delegate represents a method signature. An event stores a list of subscribed handlers that match that delegate signature.

A custom delegate example:

Code
public delegate void TemperatureChangedHandler(decimal newTemperature);

public sealed class Thermostat
{
    public event TemperatureChangedHandler? TemperatureChanged;

    public void SetTemperature(decimal value)
    {
        TemperatureChanged?.Invoke(value);
    }
}

The more common .NET style is to use EventHandler or EventHandler<TEventArgs>:

Code
public sealed class TemperatureChangedEventArgs : EventArgs
{
    public TemperatureChangedEventArgs(decimal temperature)
    {
        Temperature = temperature;
    }

    public decimal Temperature { get; }
}

public sealed class Thermostat
{
    public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;

    public void SetTemperature(decimal value)
    {
        TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(value));
    }
}

EventHandler<TEventArgs> is usually preferred because it follows .NET conventions:

Code
void Handler(object? sender, TemperatureChangedEventArgs args)
{
    Console.WriteLine(args.Temperature);
}

The sender identifies the publisher, and the event args object carries event data.

How Event Subscription Works

Subscribers use += to subscribe and -= to unsubscribe:

Code
var thermostat = new Thermostat();

void OnTemperatureChanged(object? sender, TemperatureChangedEventArgs args)
{
    Console.WriteLine($"Temperature: {args.Temperature}");
}

thermostat.TemperatureChanged += OnTemperatureChanged;
thermostat.SetTemperature(25);

thermostat.TemperatureChanged -= OnTemperatureChanged;

This matters because event subscriptions create references. If a long-lived publisher holds an event handler reference to a short-lived subscriber, the subscriber may stay alive longer than expected.

This is one of the most important practical issues in interviews.

Event Access Rules

An event can usually be raised only from inside the class that declares it.

Code
public sealed class Publisher
{
    public event EventHandler? SomethingHappened;

    public void DoWork()
    {
        SomethingHappened?.Invoke(this, EventArgs.Empty);
    }
}

External code can subscribe or unsubscribe:

Code
publisher.SomethingHappened += Handler;
publisher.SomethingHappened -= Handler;

But external code cannot directly invoke the event:

Code
// Not allowed from outside Publisher:
// publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty);

This protects the publisher from external code incorrectly raising its events.

Standard Event Pattern

The standard .NET event pattern uses:

  • An event named after what happened.
  • EventHandler when no custom data is needed.
  • EventHandler<TEventArgs> when event data is needed.
  • A protected virtual method named OnEventName in inheritable classes.

Example:

Code
public sealed class FileProcessor
{
    public event EventHandler<FileProcessedEventArgs>? FileProcessed;

    public void Process(string fileName)
    {
        // Process the file.

        OnFileProcessed(new FileProcessedEventArgs(fileName));
    }

    private void OnFileProcessed(FileProcessedEventArgs args)
    {
        FileProcessed?.Invoke(this, args);
    }
}

public sealed class FileProcessedEventArgs : EventArgs
{
    public FileProcessedEventArgs(string fileName)
    {
        FileName = fileName;
    }

    public string FileName { get; }
}

For inheritable classes, OnEventName is often protected virtual:

Code
public class FileProcessor
{
    public event EventHandler<FileProcessedEventArgs>? FileProcessed;

    protected virtual void OnFileProcessed(FileProcessedEventArgs args)
    {
        FileProcessed?.Invoke(this, args);
    }
}

This allows derived classes to customize event raising behavior.

EventHandler vs Custom Delegate

A custom delegate is useful when the event shape does not match the standard .NET event pattern, but most application code should use EventHandler<TEventArgs>.

Custom delegate:

Code
public delegate void MessageReceivedHandler(string message);

Standard event style:

Code
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;

The standard event style is usually better because it is familiar, consistent with .NET libraries, and easier for other developers to understand.

INotifyPropertyChanged

INotifyPropertyChanged is a common observer-style interface used heavily in UI frameworks and data binding. It allows an object to notify listeners when one of its properties changes.

Code
using System.ComponentModel;
using System.Runtime.CompilerServices;

public sealed class CustomerViewModel : INotifyPropertyChanged
{
    private string _name = string.Empty;

    public event PropertyChangedEventHandler? PropertyChanged;

    public string Name
    {
        get => _name;
        set
        {
            if (_name == value)
            {
                return;
            }

            _name = value;
            OnPropertyChanged();
        }
    }

    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Key points:

  • The UI subscribes to PropertyChanged.
  • The view model raises the event when a property changes.
  • CallerMemberName avoids hardcoding property names.
  • The setter checks whether the value actually changed before raising the event.

This pattern is important in WPF, MAUI, WinUI, and MVVM-style applications.

IObservable<T> and IObserver<T>

C# events are common for simple notifications. For more formal observer-style communication, .NET provides IObservable<T> and IObserver<T>.

IObservable<T> represents a provider of notifications:

Code
public interface IObservable<out T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

IObserver<T> represents a subscriber:

Code
public interface IObserver<in T>
{
    void OnNext(T value);
    void OnError(Exception error);
    void OnCompleted();
}

The three observer methods mean:

  • OnNext: new data is available.
  • OnError: the stream failed.
  • OnCompleted: the stream ended successfully.

A simple observable implementation:

Code
public sealed class PriceFeed : IObservable<decimal>
{
    private readonly List<IObserver<decimal>> _observers = new();

    public IDisposable Subscribe(IObserver<decimal> observer)
    {
        if (!_observers.Contains(observer))
        {
            _observers.Add(observer);
        }

        return new Unsubscriber(_observers, observer);
    }

    public void Publish(decimal price)
    {
        foreach (var observer in _observers.ToArray())
        {
            observer.OnNext(price);
        }
    }

    public void Complete()
    {
        foreach (var observer in _observers.ToArray())
        {
            observer.OnCompleted();
        }

        _observers.Clear();
    }

    private sealed class Unsubscriber : IDisposable
    {
        private readonly List<IObserver<decimal>> _observers;
        private readonly IObserver<decimal> _observer;

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

        public void Dispose()
        {
            _observers.Remove(_observer);
        }
    }
}

An observer implementation:

Code
public sealed class PriceLogger : IObserver<decimal>
{
    public void OnNext(decimal value)
    {
        Console.WriteLine($"New price: {value}");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine($"Error: {error.Message}");
    }

    public void OnCompleted()
    {
        Console.WriteLine("Price feed completed.");
    }
}

Usage:

Code
var feed = new PriceFeed();
var logger = new PriceLogger();

using IDisposable subscription = feed.Subscribe(logger);

feed.Publish(100.25m);
feed.Publish(101.10m);
feed.Complete();

IObservable<T> is especially useful when notifications form a stream of values over time.

Events vs IObservable<T>

Both events and IObservable<T> support observer-style communication, but they are used differently.

FeatureC# EventsIObservable<T>
Common useSimple object notificationsStreams of data over time
Subscription+= and -=Subscribe() returns IDisposable
Completion signalNot built inOnCompleted()
Error signalNot built inOnError(Exception)
Data modelIndividual event occurrencePush-based sequence
Typical examplesButton click, property changed, file processedPrice feed, sensor stream, reactive pipelines
ComplexityLowerHigher
Interview focusDelegates, event patterns, memory leaksObserver pattern, streams, disposal, reactive thinking

Use events when the notification is simple and local. Use IObservable<T> when subscribers consume a stream, need completion/error semantics, or when reactive-style operators are useful.

Events vs Direct Method Calls

Direct method calls are simple and explicit:

Code
_emailService.SendOrderConfirmation(orderId);

Observer-style communication is more flexible:

Code
OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(orderId));

Direct calls are better when:

  • The dependency is required.
  • The action is part of the core use case.
  • Failure should directly affect the operation.
  • The workflow must be easy to trace.

Observer-style communication is better when:

  • Multiple independent components may react.
  • The publisher should not know the subscriber details.
  • Subscribers may change over time.
  • The reaction is a side effect, extension point, or notification.

A common mistake is overusing events for business workflows that should be explicit. Loose coupling should not make important behavior invisible.

Events vs Callbacks

A callback is usually a method or delegate passed directly into another method:

Code
public void Download(string url, Action<int> onProgress)
{
    for (int progress = 0; progress <= 100; progress += 10)
    {
        onProgress(progress);
    }
}

Usage:

Code
Download("https://example.com/file.zip", progress =>
{
    Console.WriteLine($"Progress: {progress}%");
});

Callbacks are useful for one-off behavior. Events are better when multiple subscribers may listen over a longer lifetime.

FeatureCallbackEvent
Subscription lifetimeUsually temporaryUsually longer-lived
Number of subscribersUsually oneUsually many
OwnershipPassed by callerExposed by publisher
Common useCompletion callback, progress callbackUI events, state changes, notifications

Events vs Mediator and Pub/Sub

Observer-style events are usually in-process. They happen inside the same application memory space.

Mediator-style communication uses a mediator object to route messages between senders and handlers. This is common in CQRS architectures.

Example concept:

Code
public sealed record OrderPlacedNotification(int OrderId);

A handler might process the notification:

Code
public sealed class SendOrderEmailHandler
{
    public Task Handle(OrderPlacedNotification notification, CancellationToken cancellationToken)
    {
        // Send email.
        return Task.CompletedTask;
    }
}

Pub/sub systems, such as message brokers, are usually out-of-process. They support communication between services.

ApproachScopeCommon Use
C# eventSame object/appUI and local notifications
IObservable<T>Same appStreams of values
MediatorSame appApplication/domain notifications
Message brokerAcross processes/servicesDistributed event-driven architecture

A strong interview answer should mention that C# events are not a replacement for durable messaging. If a process crashes, an in-memory event is lost. For cross-service communication, use a message broker or event bus.

Memory Leaks from Event Subscriptions

One of the most common mistakes with C# events is forgetting to unsubscribe.

Example problem:

Code
public sealed class DashboardWidget
{
    public DashboardWidget(OrderService orderService)
    {
        orderService.OrderPlaced += OnOrderPlaced;
    }

    private void OnOrderPlaced(object? sender, OrderPlacedEventArgs args)
    {
        // Update widget.
    }
}

If OrderService lives for the entire application lifetime and DashboardWidget is temporary, the event subscription can keep DashboardWidget alive.

Better:

Code
public sealed class DashboardWidget : IDisposable
{
    private readonly OrderService _orderService;

    public DashboardWidget(OrderService orderService)
    {
        _orderService = orderService;
        _orderService.OrderPlaced += OnOrderPlaced;
    }

    private void OnOrderPlaced(object? sender, OrderPlacedEventArgs args)
    {
        // Update widget.
    }

    public void Dispose()
    {
        _orderService.OrderPlaced -= OnOrderPlaced;
    }
}

Important habit:

  • If you subscribe to an event on a longer-lived object, unsubscribe when the subscriber is disposed.
  • If publisher and subscriber have the same lifetime, explicit unsubscription may be less critical.
  • Avoid anonymous event handlers when you need to unsubscribe later.

Problematic:

Code
orderService.OrderPlaced += (sender, args) =>
{
    Console.WriteLine(args.OrderId);
};

// Hard to unsubscribe because the delegate instance is not stored.

Better:

Code
EventHandler<OrderPlacedEventArgs> handler = (sender, args) =>
{
    Console.WriteLine(args.OrderId);
};

orderService.OrderPlaced += handler;
orderService.OrderPlaced -= handler;

Weak Event Pattern

A weak event pattern avoids keeping the subscriber alive only because it subscribed to an event. This is especially relevant in UI frameworks where a long-lived event source can accidentally retain many short-lived UI objects.

A weak event uses a weak reference to the subscriber instead of a strong reference. This allows the subscriber to be garbage collected if nothing else references it.

In most everyday C# backend code, explicit unsubscription is easier and clearer. Weak events are more common in UI frameworks, component libraries, and advanced infrastructure code.

Thread Safety

Events and observer lists can create thread-safety problems.

For example, a subscriber may unsubscribe while the publisher is notifying subscribers. A common defensive approach is to copy the event delegate before invocation:

Code
var handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);

Modern C# code often uses:

Code
SomethingHappened?.Invoke(this, EventArgs.Empty);

For custom observer lists, snapshot the list before iterating:

Code
foreach (var observer in _observers.ToArray())
{
    observer.OnNext(value);
}

If multiple threads can subscribe, unsubscribe, and publish at the same time, protect the list with a lock or use an appropriate concurrent design.

Example:

Code
private readonly object _gate = new();
private readonly List<IObserver<decimal>> _observers = new();

public IDisposable Subscribe(IObserver<decimal> observer)
{
    lock (_gate)
    {
        _observers.Add(observer);
    }

    return new Unsubscriber(this, observer);
}

public void Publish(decimal value)
{
    IObserver<decimal>[] snapshot;

    lock (_gate)
    {
        snapshot = _observers.ToArray();
    }

    foreach (var observer in snapshot)
    {
        observer.OnNext(value);
    }
}

Thread safety is important in background services, UI applications, real-time updates, and server-side applications.

Exception Handling

When an event has multiple subscribers, one subscriber throwing an exception can prevent later subscribers from running.

Example:

Code
public event EventHandler? SomethingHappened;

public void Raise()
{
    SomethingHappened?.Invoke(this, EventArgs.Empty);
}

If the first handler throws, the second handler may not execute.

If each subscriber should be isolated, you can manually invoke the invocation list:

Code
public void RaiseSafely()
{
    var handlers = SomethingHappened?.GetInvocationList();

    if (handlers is null)
    {
        return;
    }

    foreach (EventHandler handler in handlers)
    {
        try
        {
            handler(this, EventArgs.Empty);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Handler failed: {ex.Message}");
        }
    }
}

This is not always the right choice. Sometimes an exception should stop the operation. The important interview point is that event exception behavior should be deliberate.

Async Event Handlers

C# events are traditionally synchronous. If an event handler is async void, exceptions can be hard to handle.

Example:

Code
button.Click += async (sender, args) =>
{
    await SaveAsync();
};

async void is common and acceptable for UI event handlers, but should generally be avoided in business logic.

For application-level async notifications, prefer explicit async methods, mediator notifications, channels, background queues, or custom async event patterns.

Example of an explicit async notification approach:

Code
public interface IOrderPlacedHandler
{
    Task HandleAsync(OrderPlacedEvent notification, CancellationToken cancellationToken);
}

public sealed record OrderPlacedEvent(int OrderId);

This is often easier to test and reason about than many async void event handlers.

Domain Events

Domain events are a design pattern used in domain-driven design. A domain event represents something important that happened in the business domain.

Examples:

  • OrderPlaced
  • PaymentReceived
  • PolicyRenewed
  • CustomerRegistered
  • InvoiceGenerated

A simple domain event:

Code
public interface IDomainEvent
{
    DateTime OccurredOnUtc { get; }
}

public sealed record OrderPlacedDomainEvent(
    int OrderId,
    DateTime OccurredOnUtc) : IDomainEvent;

An entity may collect domain events:

Code
public sealed class Order
{
    private readonly List<IDomainEvent> _domainEvents = new();

    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    public int Id { get; private set; }

    public void Place()
    {
        // Business logic.

        _domainEvents.Add(new OrderPlacedDomainEvent(Id, DateTime.UtcNow));
    }

    public void ClearDomainEvents()
    {
        _domainEvents.Clear();
    }
}

Domain events are not the same as C# events. They are usually stored as data objects and dispatched by application infrastructure after the business operation succeeds.

This is often better for Clean Architecture because domain entities stay independent from infrastructure concerns.

Observer-Style Communication in ASP.NET Core

In ASP.NET Core backend applications, C# events are less common for request workflows because web requests are short-lived and dependency lifetimes matter.

Common backend alternatives include:

  • Direct service calls for required behavior.
  • MediatR notifications or similar mediator patterns for in-process notifications.
  • Background queues for asynchronous processing.
  • Message brokers for cross-service communication.
  • Domain events for business-significant changes.
  • SignalR for real-time client notifications.

Example use cases:

ScenarioBetter Choice
Button click in UIC# event
View model property updateINotifyPropertyChanged
Background progress streamIObservable<T> or channel
Required business stepDirect service call
Domain state changeDomain event
Cross-service notificationMessage broker
Real-time browser updateSignalR
CQRS notification handlersMediator notification

Common Mistakes

Common mistakes include:

  • Using events when a direct method call would be clearer.
  • Forgetting to unsubscribe from long-lived publishers.
  • Using anonymous handlers when later unsubscription is required.
  • Putting critical hidden business logic in event handlers.
  • Not considering exception behavior across multiple handlers.
  • Assuming event handlers run asynchronously.
  • Raising events before state is fully updated.
  • Modifying the subscriber list while iterating over it.
  • Creating custom observer infrastructure without thread-safety.
  • Using in-memory events for cross-service communication.
  • Exposing mutable event data that subscribers can accidentally change.
  • Ignoring cancellation and error handling in async workflows.

Best Practices

Good habits include:

  • Use EventHandler or EventHandler<TEventArgs> for normal .NET events.
  • Name events after something that happened, such as OrderPlaced, not PlaceOrder.
  • Use immutable event data when possible.
  • Raise events after the publisher reaches a consistent state.
  • Keep event handlers small and focused.
  • Unsubscribe from longer-lived publishers.
  • Implement IDisposable when an object owns event subscriptions that must be released.
  • Prefer explicit service calls for required business behavior.
  • Prefer domain events or mediator notifications for application-level business events.
  • Prefer message brokers for distributed communication.
  • Consider thread safety when events can be raised from multiple threads.
  • Consider exception isolation if one bad subscriber should not block others.
  • Avoid async void except for UI event handlers.
  • Use IObservable<T> when modeling streams of values over time.
  • Avoid overengineering simple code with unnecessary observer abstractions.

Practical Example: Event-Based Notification

Code
public sealed class PaymentService
{
    public event EventHandler<PaymentCompletedEventArgs>? PaymentCompleted;

    public void CompletePayment(int paymentId, decimal amount)
    {
        // Validate and persist payment.

        PaymentCompleted?.Invoke(
            this,
            new PaymentCompletedEventArgs(paymentId, amount));
    }
}

public sealed class PaymentCompletedEventArgs : EventArgs
{
    public PaymentCompletedEventArgs(int paymentId, decimal amount)
    {
        PaymentId = paymentId;
        Amount = amount;
    }

    public int PaymentId { get; }
    public decimal Amount { get; }
}

Subscriber:

Code
public sealed class ReceiptEmailSender : IDisposable
{
    private readonly PaymentService _paymentService;

    public ReceiptEmailSender(PaymentService paymentService)
    {
        _paymentService = paymentService;
        _paymentService.PaymentCompleted += OnPaymentCompleted;
    }

    private void OnPaymentCompleted(object? sender, PaymentCompletedEventArgs args)
    {
        Console.WriteLine($"Send receipt for payment {args.PaymentId}");
    }

    public void Dispose()
    {
        _paymentService.PaymentCompleted -= OnPaymentCompleted;
    }
}

This is simple and useful when everything runs in the same process.

Practical Example: Domain Event Instead of C# Event

Code
public sealed record PaymentCompletedDomainEvent(
    int PaymentId,
    decimal Amount,
    DateTime OccurredOnUtc);

public sealed class Payment
{
    private readonly List<PaymentCompletedDomainEvent> _events = new();

    public IReadOnlyCollection<PaymentCompletedDomainEvent> Events => _events;

    public int Id { get; private set; }
    public decimal Amount { get; private set; }
    public bool IsCompleted { get; private set; }

    public void Complete()
    {
        if (IsCompleted)
        {
            return;
        }

        IsCompleted = true;

        _events.Add(new PaymentCompletedDomainEvent(
            Id,
            Amount,
            DateTime.UtcNow));
    }
}

This approach is often better in business applications because the event is represented as data. Application infrastructure can dispatch it after saving changes.

Practical Example: IObservable<T> Stream

Code
public sealed class ProgressObserver : IObserver<int>
{
    public void OnNext(int value)
    {
        Console.WriteLine($"Progress: {value}%");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine($"Failed: {error.Message}");
    }

    public void OnCompleted()
    {
        Console.WriteLine("Completed.");
    }
}

A stream-like provider can push multiple values over time:

Code
public sealed class ProgressReporter : IObservable<int>
{
    private readonly List<IObserver<int>> _observers = new();

    public IDisposable Subscribe(IObserver<int> observer)
    {
        _observers.Add(observer);
        return new Subscription(_observers, observer);
    }

    public void Report(int progress)
    {
        foreach (var observer in _observers.ToArray())
        {
            observer.OnNext(progress);
        }
    }

    public void Complete()
    {
        foreach (var observer in _observers.ToArray())
        {
            observer.OnCompleted();
        }
    }

    private sealed class Subscription : IDisposable
    {
        private readonly List<IObserver<int>> _observers;
        private readonly IObserver<int> _observer;

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

        public void Dispose()
        {
            _observers.Remove(_observer);
        }
    }
}

Usage:

Code
var reporter = new ProgressReporter();
var observer = new ProgressObserver();

using var subscription = reporter.Subscribe(observer);

reporter.Report(10);
reporter.Report(50);
reporter.Report(100);
reporter.Complete();

Interview Decision Guide

A good decision process is:

  1. Use a direct method call if the dependency is required and part of the main workflow.
  2. Use a C# event for simple local notifications.
  3. Use INotifyPropertyChanged for property change notifications in data binding.
  4. Use IObservable<T> for streams of values over time.
  5. Use domain events for business-significant changes inside the domain model.
  6. Use mediator notifications for in-process application events.
  7. Use message brokers for cross-service or durable event-driven communication.
  8. Use SignalR when server-side events must be pushed to connected clients.

The best choice depends on lifetime, coupling, reliability, threading, error handling, and whether the communication is in-process or distributed.

Interview Practice

PreviousLINQ QueryingNext UpPattern Matching in C#