Skip to main content
Event handling is a mechanism that enables objects to notify other objects when something of interest occurs, following the publisher-subscriber pattern (also known as the observer pattern). Its core purpose is to establish loose coupling between components.

Event Declarations

Event declarations define the contract for notifications. In C#, events are built on the delegate foundation and use the event keyword to create a special type of member that can only be invoked by the containing class.
public class TemperatureSensor
{
    // 1. Define a delegate type that matches the event signature
    public delegate void TemperatureChangedHandler(object sender, TemperatureChangedEventArgs e);
    
    // 2. Declare the event using the delegate type
    public event TemperatureChangedHandler TemperatureChanged;
    
    private double _currentTemperature;
    
    public double CurrentTemperature
    {
        get => _currentTemperature;
        set
        {
            if (_currentTemperature != value)
            {
                _currentTemperature = value;
                
                // 3. Create event arguments
                var args = new TemperatureChangedEventArgs(_currentTemperature);
                
                // 4. Raise the event (null check required)
                TemperatureChanged?.Invoke(this, args);
            }
        }
    }
}

// Custom event arguments class
public class TemperatureChangedEventArgs : EventArgs
{
    public double NewTemperature { get; }
    
    public TemperatureChangedEventArgs(double newTemperature)
    {
        NewTemperature = newTemperature;
    }
}
The event keyword provides encapsulation, preventing external code from directly invoking the delegate.

Thread-Safe Events

Thread safety ensures that events can be safely used in multi-threaded scenarios where subscribers might be added/removed concurrently with event invocation.
public class ThreadSafePublisher
{
    // Use EventHandler pattern for better thread safety
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
    
    private readonly object _eventLock = new object();
    private double _temperature;
    
    public double Temperature
    {
        get => _temperature;
        set
        {
            _temperature = value;
            RaiseTemperatureChanged(value);
        }
    }
    
    private void RaiseTemperatureChanged(double newTemp)
    {
        // Thread-safe event invocation pattern
        var handlers = TemperatureChanged;
        if (handlers != null)
        {
            var args = new TemperatureChangedEventArgs(newTemp);
            handlers(this, args);
        }
        
        // Alternative: Using null-conditional operator (C# 6+)
        // TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
    }
    
    // Thread-safe subscription management
    public void AddSubscriber(EventHandler<TemperatureChangedEventArgs> handler)
    {
        lock (_eventLock)
        {
            TemperatureChanged += handler;
        }
    }
    
    public void RemoveSubscriber(EventHandler<TemperatureChangedEventArgs> handler)
    {
        lock (_eventLock)
        {
            TemperatureChanged -= handler;
        }
    }
}
Memory Leaks: Event subscriptions create strong references. Failing to unsubscribe can lead to memory leaks. Implement IDisposable for cleanup.

EventHandler Patterns

C# provides standardized patterns for event handling that promote consistency and interoperability.
// Pattern 1: Standard EventHandler\<T\> pattern (Recommended)
public class StandardEventPattern
{
    // Uses the built-in EventHandler\<T\> delegate with EventArgs
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
    
    protected virtual void OnTemperatureChanged(double newTemp)
    {
        TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
    }
}

// Pattern 2: Custom delegate with event accessors
public class CustomAccessorPattern
{
    private EventHandler<TemperatureChangedEventArgs> _temperatureChanged;
    
    // Custom add/remove accessors for additional control
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged
    {
        add
        {
            // Add validation, logging, or synchronization
            lock (this)
            {
                _temperatureChanged += value;
            }
        }
        remove
        {
            lock (this)
            {
                _temperatureChanged -= value;
            }
        }
    }
    
    protected virtual void OnTemperatureChanged(double newTemp)
    {
        _temperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
    }
}

// Pattern 3: EventHandler (non-generic) for simple scenarios
public class SimpleEventPattern
{
    public event EventHandler TemperatureChanged;
    
    protected virtual void OnTemperatureChanged()
    {
        TemperatureChanged?.Invoke(this, EventArgs.Empty);
    }
}
Best Practice: Use the built-in EventHandler<TEventArgs> pattern instead of defining custom delegates unless you need specific naming for clarity.

Why Event Handling is Important

  1. Open/Closed Principle (SOLID): Events allow extending object behavior without modifying existing code
  2. Loose Coupling: Publishers and subscribers remain independent, reducing dependencies
  3. Scalability: Event-driven architectures handle complex interaction patterns efficiently

Advanced Nuances

Async Event Handlers and Exception Handling

Async event handlers require special consideration:
public class AsyncEventPattern
{
    public event Func<object, EventArgs, Task> AsyncEvent;
    
    public async Task RaiseAsyncEvent()
    {
        var handlers = AsyncEvent;
        if (handlers != null)
        {
            var tasks = handlers.GetInvocationList()
                .Cast<Func<object, EventArgs, Task>>()
                .Select(handler => handler(this, EventArgs.Empty));
            
            // Wait for all handlers to complete
            await Task.WhenAll(tasks);
        }
    }
}

// Exception handling in async events
public class RobustAsyncEvent
{
    public event Func<object, EventArgs, Task> AsyncEvent;
    
    public async Task RaiseSafely()
    {
        var handlers = AsyncEvent;
        if (handlers != null)
        {
            var tasks = handlers.GetInvocationList()
                .Cast<Func<object, EventArgs, Task>>()
                .Select(async handler =>
                {
                    try
                    {
                        await handler(this, EventArgs.Empty);
                    }
                    catch (Exception ex)
                    {
                        // Log error but don't break other handlers
                        Console.WriteLine($"Handler failed: {ex.Message}");
                    }
                });
            
            await Task.WhenAll(tasks);
        }
    }
}

Event Aggregators for Complex Systems

// Event aggregator pattern for decoupled communication
public class EventAggregator
{
    private readonly Dictionary<Type, List<Delegate>> _handlers = new();
    
    public void Subscribe<TEvent>(Action<TEvent> handler)
    {
        var eventType = typeof(TEvent);
        if (!_handlers.ContainsKey(eventType))
            _handlers[eventType] = new List<Delegate>();
        
        _handlers[eventType].Add(handler);
    }
    
    public void Publish<TEvent>(TEvent eventData)
    {
        if (_handlers.TryGetValue(typeof(TEvent), out var handlers))
        {
            // Create copy to avoid modification during iteration
            foreach (var handler in handlers.ToArray())
            {
                ((Action<TEvent>)handler)(eventData);
            }
        }
    }
}

Roadmap Context

Event handling sits as the practical application layer in the “Delegates and Events” section. It builds upon delegate fundamentals and unlocks:
  • Reactive Extensions (Rx.NET): Event streams and LINQ-like event processing
  • Async event patterns: Advanced asynchronous programming techniques
  • Dependency Injection integration: Event handling in IoC containers
  • Message buses and event sourcing: Distributed event-driven architectures

Build docs developers (and LLMs) love