How Custom Event Accessors Work in C
Custom event accessors in C# allow developers to override the default
behavior of event subscription and unsubscription by explicitly defining
add and remove blocks. Similar to how property
accessors (get and set) control access to
private fields, event accessors give you fine-grained control over how
delegate handlers are stored, synchronized, and executed. This article
explains how custom event accessors function, their syntax, common use
cases like memory optimization and thread safety, and key implementation
considerations.
The Basics of Event Accessors
When you declare a standard field-like event in C#:
public event EventHandler ProcessCompleted;The C# compiler automatically generates a private delegate field
behind the scenes along with thread-safe add and
remove methods using
Interlocked.CompareExchange.
When you define custom event accessors, you take manual control over
these operations by writing explicit add and
remove blocks.
public class ProcessManager
{
private EventHandler? _processCompleted;
public event EventHandler ProcessCompleted
{
add
{
_processCompleted += value;
}
remove
{
_processCompleted -= value;
}
}
}Within these blocks, the implicit contextual keyword
value represents the delegate handler being added or
removed.
When to Use Custom Event Accessors
1. Memory Optimization for Sparse Events
In graphical frameworks or systems with classes defining dozens or hundreds of events, creating a separate delegate field for every event can consume significant memory, especially if most instances subscribe to only a few events.
Custom accessors can map event registrations to a shared dictionary
or System.ComponentModel.EventHandlerList:
using System.ComponentModel;
public class ComponentWithManyEvents
{
private readonly EventHandlerList _events = new EventHandlerList();
private static readonly object ClickEventKey = new object();
public event EventHandler Click
{
add => _events.AddHandler(ClickEventKey, value);
remove => _events.RemoveHandler(ClickEventKey, value);
}
protected virtual void OnClick(EventArgs e)
{
((EventHandler?)_events[ClickEventKey])?.Invoke(this, e);
}
}2. Thread Safety and Synchronization
If your application requires custom synchronization mechanisms, such
as logging subscriptions or wrapping access with custom locking
mechanisms (e.g., lock or
ReaderWriterLockSlim), custom accessors allow you to
control thread synchronization:
public class SafeNotifier
{
private EventHandler? _notifier;
private readonly object _lock = new object();
public event EventHandler Notifier
{
add
{
lock (_lock)
{
_notifier += value;
}
}
remove
{
lock (_lock)
{
_notifier -= value;
}
}
}
}3. Forwarding and Interception
Custom accessors allow an event to act as a facade that forwards subscriptions to an underlying object or adds logging and validation logic during the subscription lifecycle:
public class ServiceWrapper
{
private readonly UnderlyingService _service = new UnderlyingService();
public event EventHandler StatusChanged
{
add
{
Console.WriteLine("Subscriber added.");
_service.StatusChanged += value;
}
remove
{
Console.WriteLine("Subscriber removed.");
_service.StatusChanged -= value;
}
}
}Rules and Constraints
- Both Accessors Required: You cannot provide only an
addor only aremoveaccessor. Both must be implemented. - No Direct Invocation: You cannot invoke the event using its public identifier if you define custom accessors, because the compiler does not generate a backing delegate field. You must maintain and invoke your own underlying delegate or collection.
- Thread Safety Responsibility: Removing the default implementation shifts full responsibility for thread safety to you. Without proper synchronization, concurrent subscriptions or unsubscriptions can lead to race conditions.