Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

An event in C# is a class or struct member that provides notifications to subscribers. Declared using the event modifier, it provides an encapsulation layer over a multicast delegate, ensuring that external entities can only subscribe (+=) or unsubscribe (-=) from the delegate, while strictly confining the invocation privilege to the declaring type. Without the event modifier, a public delegate field could be invoked, reassigned, or cleared by any external class. The event keyword enforces access control, preventing external code from breaking the invocation chain.

Compiler Mechanics

The underlying implementation of an event depends on its declaration syntax. For standard, field-like events, the C# compiler automatically generates three components under the hood:
  1. A private delegate backing field: This stores the invocation list of subscribed methods.
  2. An add accessor: A method that safely appends a delegate to the backing field’s invocation list using Delegate.Combine.
  3. A remove accessor: A method that safely removes a delegate from the backing field’s invocation list using Delegate.Remove.
By default, the compiler-generated add and remove accessors use thread-safe operations (via Interlocked.CompareExchange) to prevent race conditions during concurrent subscription modifications. If the event is explicitly implemented (using custom accessors), the compiler only generates the add and remove methods. It does not generate a backing field, leaving the developer responsible for defining and managing the underlying delegate storage.

Standard Event Syntax

The most common way to declare an event is using a field-like syntax. The standard .NET convention utilizes the built-in EventHandler delegate when no custom event data is passed, and EventHandler<TEventArgs> when passing custom event arguments.
public class Publisher
{
    // 1. Event declaration using standard non-generic EventHandler
    public event EventHandler OperationCompleted;

    // 2. Protected virtual method to handle invocation (Standard .NET Pattern)
    protected virtual void OnOperationCompleted(EventArgs e)
    {
        // Null-conditional operator prevents NullReferenceException 
        // if the invocation list is empty (no subscribers).
        OperationCompleted?.Invoke(this, e);
    }

    public void DoWork()
    {
        // Internal logic here...
        OnOperationCompleted(EventArgs.Empty);
    }
}

public class Subscriber
{
    public void Subscribe(Publisher publisher)
    {
        // External classes can ONLY use += or -=
        publisher.OperationCompleted += HandleOperationCompleted;
    }

    private void HandleOperationCompleted(object sender, EventArgs e)
    {
        // Callback execution logic
    }
}

Explicit Event Accessors

If you need to control the underlying storage of the delegate (e.g., to optimize memory when a class exposes dozens of events but only a few are subscribed to), you can explicitly implement the add and remove accessors. This is syntactically similar to property getters and setters. When implementing custom thread safety, a dedicated private locking object must be used to prevent deadlocks, avoiding anti-patterns like locking on this.
public class CustomAccessorPublisher
{
    // Dedicated locking object to prevent deadlocks
    private readonly object _eventLock = new object();
    
    // Explicit backing field
    private EventHandler _operationCompleted;

    // Explicit event declaration
    public event EventHandler OperationCompleted
    {
        add
        {
            // Custom logic (e.g., custom locking, logging, or dictionary storage)
            lock (_eventLock)
            {
                _operationCompleted += value;
            }
        }
        remove
        {
            lock (_eventLock)
            {
                _operationCompleted -= value;
            }
        }
    }

    protected virtual void OnOperationCompleted(EventArgs e)
    {
        _operationCompleted?.Invoke(this, e);
    }
}

Memory Management Implications

Events inherently create a strong reference from the publisher to the subscriber. When a subscriber registers a method to an event, the publisher’s delegate backing field holds a reference to the subscriber’s instance (via the delegate’s Target property). If the publisher’s lifecycle outlives the subscriber, the garbage collector cannot reclaim the subscriber’s memory until the publisher is also collected or the subscriber explicitly unsubscribes using the -= operator. This is the most common cause of memory leaks in .NET applications.
Master C# with Deep Grasping Methodology!Learn More