← Open in the full interactive course (progress tracking, search & more)

A delegate variable doesn't have to hold just one method — it can hold a whole list of them, and call every one.

In the last lesson you defined a delegate type and pointed a single variable at a single method. That's already useful — but real applications often need more: several independent pieces of code reacting to the same thing.

Think about saving a document. You might want to: write the file to disk, update the "last saved" timestamp in the UI, and log the save for auditing. Three unrelated actions, all triggered by the same moment. Do you really want Save() to know about the UI and the logger and the disk, all mixed together?

C# delegates have a feature built in for exactly this: a single delegate variable can reference more than one method at once, and invoking it runs all of them in order. This is called a multicast delegate, and it's the mechanism that makes C# events possible.

What Is It?

The Simple Explanation

A custom delegate is simply a delegate type you declare yourself, with a signature that matches your specific need — you saw this already with MathOperation. A multicast delegate is a delegate variable holding not one method, but a chain of them, all with the same signature. Invoking it calls each one, in the order they were added.

The Technical Definition

Every delegate type you declare in C# actually derives from System.MulticastDelegate, which itself derives from System.Delegate. This means every delegate you create can hold multiple methods in its invocation list — a single method is just the special case of an invocation list with one entry.

You build up that list with the += operator (add a method) and shrink it with -= (remove a method):

public delegate void Notify(string message);

Notify onSave = LogSave;
onSave += UpdateUi;
onSave += AuditSave;

onSave("Document saved."); // calls LogSave, then UpdateUi, then AuditSave

Why Does It Exist?

The Problem — One Trigger, Many Independent Reactions

Without multicast delegates, "notify several unrelated pieces of code that something happened" means either:

The Solution — Built-In Subscriber Lists

Multicast delegates give you that "list of subscribers" behavior for free. += and -= are just operator overloads that add to or remove from the invocation list — no manual list management required. This is exactly the plumbing that C#'s event keyword (covered in lesson 105) builds on: an event is a multicast delegate with extra access restrictions.

The key insight

Multicast delegates let one action trigger many independent reactions, without any of those reactions knowing about each other, and without the triggering code knowing what they do. This is the seed of publish/subscribe — a pattern you'll use constantly once you reach events.

Big Picture

ONE INVOCATION → MANY METHODS
onSave("Document saved.")
1. LogSave
2. UpdateUi
3. AuditSave
Invoked in the order they were added, one after another, on the same thread.

How It Works

BUILDING AND USING A MULTICAST DELEGATE
1. DECLARE THE DELEGATE TYPE
public delegate void Notify(string message);
2. ADD SUBSCRIBERS WITH +=
Notify onSave = null;
onSave += LogSave;
onSave += UpdateUi;
3. INVOKE — RUNS EVERY SUBSCRIBER IN ORDER
onSave("Document saved.");
// LogSave runs, then UpdateUi runs
4. REMOVE A SUBSCRIBER WITH -=
onSave -= UpdateUi;
onSave("Document saved."); // only LogSave runs now

Simple Example

using System;

public delegate void Notify(string message);

class Program
{
    static void LogSave(string message) => Console.WriteLine($"[LOG] {message}");
    static void UpdateUi(string message) => Console.WriteLine($"[UI] Showing: {message}");
    static void AuditSave(string message) => Console.WriteLine($"[AUDIT] Recorded: {message}");

    static void Main()
    {
        Notify onSave = LogSave;
        onSave += UpdateUi;
        onSave += AuditSave;

        onSave("Document saved.");
        // [LOG] Document saved.
        // [UI] Showing: Document saved.
        // [AUDIT] Recorded: Document saved.

        onSave -= UpdateUi; // stop updating the UI
        Console.WriteLine("--- after removing UpdateUi ---");
        onSave("Document saved again.");
        // [LOG] Document saved again.
        // [AUDIT] Recorded: Document saved again.
    }
}

Code → Meaning → Result: Each += appends a method to onSave's invocation list. A single call, onSave(...), fans out to every subscriber currently on the list, in the order they were added. Removing a subscriber with -= takes it out of future invocations without touching the others.

Real-World Example

Consider a background job runner that needs to report progress to multiple listeners — a console logger, a progress bar, and a metrics collector — without knowing any of them exist.

using System;

public delegate void ProgressHandler(int percentComplete);

public class FileImportJob
{
    public ProgressHandler? OnProgress; // multicast delegate field

    public void Run()
    {
        for (int percent = 0; percent <= 100; percent += 25)
        {
            // simulate work...
            OnProgress?.Invoke(percent); // notify every subscriber, safely
        }
    }
}

class Program
{
    static void Main()
    {
        var job = new FileImportJob();

        job.OnProgress += p => Console.WriteLine($"Console: {p}% done");
        job.OnProgress += p => Console.Title = $"Importing... {p}%";
        job.OnProgress += RecordMetric;

        job.Run();
    }

    static void RecordMetric(int percent)
    {
        if (percent == 100)
            Console.WriteLine("Metrics: import completed.");
    }
}

Notice OnProgress?.Invoke(percent) — the null-conditional operator guards against the field still being null if nobody has subscribed yet, avoiding the classic NullReferenceException from the previous lesson's "Common Mistakes." This pattern is everywhere in real event-driven code.

Analogy

A Mailing List

A single-method delegate is like a letter addressed to one person. A multicast delegate is a mailing list: you += to subscribe an address, -= to unsubscribe, and sending one message (invoking the delegate) delivers a copy to every address currently on the list — in signup order.

The sender doesn't maintain the list of subscribers by hand, and doesn't know or care who's on it. That's exactly the decoupling multicast delegates provide.

Under the Hood

THE INVOCATION LIST
1. DELEGATES ARE IMMUTABLE
2. GetInvocationList()
3. INVOCATION IS SEQUENTIAL, ON ONE THREAD

Common Confusion

1. "+= modifies the delegate" — not exactly

It looks like mutation, but as explained above, each += produces a new delegate object under the hood. This matters if you ever capture a delegate reference in another variable before adding a subscriber — that older reference still points at the shorter, original invocation list.

2. An unhandled exception in one subscriber kills the rest

Because invocation is a simple sequential loop, a throwing subscriber stops the chain — subscribers registered after it never run. If you need every subscriber to run regardless of failures, you must call GetInvocationList() and invoke each one yourself inside a try/catch.

3. Removing a method you never exactly added does nothing (silently)

-= removes the last matching entry from the invocation list based on target + method — if the method you're trying to remove isn't in the list, -= is a silent no-op, not an error.

Common Mistakes

Mistake 1 — Relying on a non-void multicast delegate's return value

Assuming result = multicastFunc(x) reflects all subscribers when only the last one's return value actually comes through.

For multicast scenarios, prefer void-returning delegates (or events, covered soon). If you truly need every result, call GetInvocationList() and collect each one explicitly.

Mistake 2 — Forgetting to unsubscribe

Adding a subscriber with += and never removing it when the subscribing object is no longer needed keeps that object alive (referenced) — a common source of memory leaks in long-lived publishers (more in lesson 106).

Pair every += with a matching -= once the subscriber no longer needs to listen.

Mistake 3 — Not guarding against a null invocation list before invoking

Calling onSave("x") directly on a field that no one has subscribed to yet throws a NullReferenceException.

Use onSave?.Invoke("x") — it's a habit worth building now, before you reach events, where it's essential.

When Should I Use It?

Mental Model

Every delegate is secretly a list of methods, even when it holds only one.
+= appends. -= removes.
Invoking it calls every method on the list, in order, on the calling thread.

This "one trigger, many subscribers" mechanism is exactly what powers C# events.

Key Takeaway


Check Your Understanding

You've seen how a single delegate can hold and invoke multiple methods. Let's check what you've learned.

1. What does the += operator do to a delegate variable?

Show answer

Correct: B

Why B is correct: Delegate instances are immutable in .NET. += is shorthand for combining delegates with Delegate.Combine and reassigning the result to the variable.

Why A is incorrect: Nothing about the original delegate object is mutated; a new one is produced.

Why C is incorrect: That would discard existing subscribers, which is not what += does — it appends.

Why D is incorrect: += works on any delegate type, regardless of return type, though only the last subscriber's return value is observable through direct invocation.

Reinforcement: Delegate immutability is why += and -= must be assignments, not in-place mutations.

2. A multicast delegate has three subscribers: A, B, and C, added in that order. Subscriber B throws an exception when invoked. What happens?

Show answer

Correct: B

Why B is correct: Multicast delegate invocation is a simple sequential loop on the calling thread. An unhandled exception in one subscriber stops the loop immediately and propagates out — subsequent subscribers are never reached.

Why A is incorrect: Nothing in the default invocation mechanism swallows exceptions; you'd need to invoke each subscriber yourself with individual try/catch blocks to get that behavior.

Why C is incorrect: Subscribers run sequentially on one thread by default, not in parallel.

Why D is incorrect: There is no automatic retry mechanism built into delegate invocation.

Reinforcement: If you need all subscribers to run regardless of individual failures, iterate GetInvocationList() and wrap each call yourself.

3. Why is onSave?.Invoke("message") generally preferred over onSave("message") for a multicast delegate field?

Show answer

Correct: B

Why B is correct: If nobody has subscribed, the delegate field is still null. Calling it directly throws; the null-conditional operator short-circuits to doing nothing instead.

Why A is incorrect: The null-conditional operator has no effect on the sequential-vs-parallel execution model.

Why C is incorrect: It has nothing to do with exception handling inside subscribers.

Why D is incorrect: Return value capture behavior (only the last subscriber's result) is unaffected by null-conditional invocation.

Reinforcement: Guarding invocation with ?.Invoke(...) is standard practice for any delegate field that might have zero subscribers.

4. You need a delegate call that must return a value from every one of several subscribers, not just the last one. What should you do?

Show answer

Correct: B

Why B is correct: GetInvocationList() exposes each subscriber as an individual delegate; invoking them separately lets you capture every return value.

Why A is incorrect: Direct invocation of a multicast delegate only surfaces the return value of the last subscriber called; the rest are discarded.

Why C is incorrect: It's entirely possible — it just requires the explicit invocation-list approach rather than a single direct call.

Why D is incorrect: Making the delegate void means there's no return value at all, which doesn't solve the stated problem.

Reinforcement: Direct invocation is convenient but lossy for non-void multicast delegates — GetInvocationList() is the escape hatch when every result matters.

You now understand multicast delegates — the exact mechanism behind C# events. Next, you'll learn about Action, the built-in delegate type that means you'll rarely need to declare a custom void-returning delegate again.


dotnetmadeeasy.com — Learn C# and .NET, the right way.