How Multicast Delegates Work in C

In C#, a multicast delegate is a delegate that holds references to more than one method, allowing multiple methods to be executed through a single invocation. When delegates are chained together using the addition assignment operator (+=), the runtime maintains an internal invocation list to track each target method. This article explains how multicast delegates are constructed, the sequential order in which methods execute, how return values and exceptions are handled, and how to safely manage invocation lists when custom behavior is required.

The Invocation List and the Delegate Type

All delegates in modern C# implicitly derive from System.MulticastDelegate, which itself inherits from System.Delegate. The core mechanism behind chaining is the _invocationList object array maintained by the MulticastDelegate class.

When you instantiate a delegate, its invocation list initially contains just that single method. When you use the += operator, the C# compiler translates this into a call to Delegate.Combine. This method returns a new delegate instance containing an array with references to all combined methods, leaving the original delegates immutable. Conversely, using the -= operator calls Delegate.Remove, which searches the invocation list from the end and removes the last matching method reference.

Action notify = SendEmail;
notify += LogToDatabase; // Delegate.Combine under the hood
notify(); // Both methods will be invoked

Execution Order

When a multicast delegate is invoked, the runtime iterates through its internal invocation list sequentially. The methods are called synchronously in the exact order they were appended (first-in, first-out). Control passes to the first method, waits for it to complete, passes to the second method, and continues until every method in the list has executed.

Handling Return Values

While multicast delegates commonly have a void return type, C# permits non-void multicast delegates. However, chaining methods with return values introduces a significant limitation: the caller only receives the return value of the last method in the invocation list.

Every preceding method still runs, but its returned value is discarded. Because of this behavior, chaining delegates with non-void return types is generally discouraged unless paired with manual iteration.

Func<int> getNumber = () => 5;
getNumber += () => 10;

int result = getNumber(); // result is 10; 5 is evaluated but discarded

Exception Propagation

A critical consideration when chaining methods is exception handling. If any method in the invocation list throws an unhandled exception:

  1. The invocation halts immediately.
  2. The exception bubbles up to the caller.
  3. Any subsequent methods remaining in the invocation list are never executed.

This behavior can leave an application in an inconsistent state, particularly when using delegates for event handling where all subscribers are expected to receive notifications.

Manual Invocation Using GetInvocationList()

To overcome the limitations regarding discarded return values and interrupted execution chains, you can manually iterate through the delegate's invocation list using the GetInvocationList() method.

GetInvocationList() returns an array of Delegate instances representing the chain. By iterating through this array with a foreach loop, you can wrap each call in a try-catch block to ensure all methods run even if one fails, or collect the return value from every target:

Func<int> getNumber = () => 5;
getNumber += () => 10;

List<int> results = new List<int>();

foreach (Func<int> handler in getNumber.GetInvocationList())
{
    try
    {
        results.Add(handler());
    }
    catch (Exception ex)
    {
        // Handle exception for this specific method without halting the loop
    }
}
// 'results' now contains both 5 and 10

By leveraging Delegate.Combine, sequential invocation lists, and defensive handling via GetInvocationList(), multicast delegates provide the foundational architecture for the publish-subscribe pattern and event-driven programming across the .NET runtime.