Skip to content

C# Delegates and Events — Delegate Types, Multicast, Event, and EventHandler

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C# Delegates and Events. We cover key concepts, practical examples, and best practices to help you master this topic.

C# delegates are type-safe references to methods that enable callback mechanisms, with events building on delegates to provide a standardized publish-subscribe notification pattern.

What You'll Learn

You will master delegates and events in C#: declaring and using delegate types, multicast delegates that invoke multiple methods, the event keyword for controlled subscription, the EventHandler delegate pattern, and how .NET uses events throughout the framework.

Why It Matters

Delegates and events are fundamental to the .NET Framework. Events power UI frameworks (button clicks, key presses), ASP.NET Core middleware pipelines, and system notifications. Delegates enable LINQ (Func/Action delegates), asynchronous programming, and callback patterns. Understanding these patterns is essential for writing decoupled, extensible code.

Real-World Use

UI applications use events for button clicks, text changes, and window events. ASP.NET Core uses delegates in middleware pipelines. Timer callbacks use EventHandler. Custom events are used in domain-driven design for domain events. Delegates are used for plug-in architectures and Strategy patterns.

Learning Path

graph LR
    A["22: LINQ Advanced"] --> B["23: Delegates & Events"]
    B --> C["24: Lambdas"]
    C --> D["25: Extension Methods"]
    D --> E["26: Nullable Reference Types"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Delegate Basics

// Declare a delegate type
public delegate void LogHandler(string message);

// Method matching the delegate signature
public static void ConsoleLogger(string message)
{
    Console.WriteLine($"[Console] {message}");
}

public static void FileLogger(string message)
{
    File.AppendAllText("log.txt", $"[File] {message}\n");
}

// Usage
LogHandler logger = ConsoleLogger;
logger("Application started");  // Invokes ConsoleLogger

logger = FileLogger;
logger("Error occurred");  // Invokes FileLogger

Multicast Delegates

A single delegate can invoke multiple methods:

LogHandler multiLogger = ConsoleLogger;
multiLogger += FileLogger;
multiLogger += (msg) => Console.WriteLine($"[Lambda] {msg}");

multiLogger("Event occurred");
// Invokes all three in order of subscription

// Removing handlers
multiLogger -= FileLogger;
multiLogger("After removal");
// Invokes ConsoleLogger and Lambda only

// Get invocation list
foreach (LogHandler handler in multiLogger.GetInvocationList())
{
    Console.WriteLine($"  Registered handler: {handler.Method.Name}");
}

Built-in Delegate Types: Func, Action, Predicate

// Action: returns void
Action sayHello = () => Console.WriteLine("Hello!");
Action<string> log = msg => Console.WriteLine(msg);
Action<int, int> printSum = (a, b) => Console.WriteLine(a + b);

sayHello();    // Hello!
log("Test");   // Test
printSum(3, 4); // 7

// Func: takes parameters, returns value
Func<int, int, int> add = (a, b) => a + b;
Func<double, double> square = x => x * x;
Func<string, int> parse = int.Parse;

Console.WriteLine(add(5, 3));      // 8
Console.WriteLine(square(4.0));    // 16
Console.WriteLine(parse("42"));    // 42

// Predicate: returns bool
Predicate<int> isEven = x => x % 2 == 0;
Predicate<string> isLong = s => s.Length > 10;

Console.WriteLine(isEven(4));    // True
Console.WriteLine(isLong("Hi")); // False

The Event Keyword

Events restrict delegate access: only the declaring class can invoke the delegate; subscribers can only add/remove handlers.

public class Button
{
    // Event declaration (uses EventHandler<T> pattern)
    public event EventHandler? Clicked;

    // Custom event with custom delegate
    public event EventHandler<KeyEventArgs>? KeyPressed;

    // Method that raises the event
    public void Click()
    {
        Console.WriteLine("Button clicked, raising event...");
        Clicked?.Invoke(this, EventArgs.Empty);
    }

    public void PressKey(char key)
    {
        KeyPressed?.Invoke(this, new KeyEventArgs(key));
    }
}

public class KeyEventArgs : EventArgs
{
    public char Key { get; }
    public KeyEventArgs(char key) => Key = key;
}

// Usage
var button = new Button();

// Subscribe
button.Clicked += (sender, args) => Console.WriteLine("  Handler 1: Button was clicked!");
button.Clicked += (sender, args) => Console.WriteLine("  Handler 2: Logging click event");

button.Click();
// Output:
// Button clicked, raising event...
//   Handler 1: Button was clicked!
//   Handler 2: Logging click event

EventHandler Pattern

The standard .NET event pattern:

public class OrderService
{
    // Standard event pattern
    public event EventHandler<OrderProcessedEventArgs>? OrderProcessed;

    public void ProcessOrder(int orderId, decimal amount)
    {
        Console.WriteLine($"Processing order #{orderId}...");

        // Business logic
        var args = new OrderProcessedEventArgs
        {
            OrderId = orderId,
            Amount = amount,
            ProcessedAt = DateTime.UtcNow,
            Success = true
        };

        // Raise event (null check for no subscribers)
        OnOrderProcessed(args);
    }

    // Protected virtual method for raising (inheritance-friendly)
    protected virtual void OnOrderProcessed(OrderProcessedEventArgs e)
    {
        OrderProcessed?.Invoke(this, e);
    }
}

public class OrderProcessedEventArgs : EventArgs
{
    public int OrderId { get; init; }
    public decimal Amount { get; init; }
    public DateTime ProcessedAt { get; init; }
    public bool Success { get; init; }
    public string? ErrorMessage { get; init; }
}

// Subscriber
var orderService = new OrderService();
orderService.OrderProcessed += (sender, e) =>
{
    string status = e.Success ? "succeeded" : "failed";
    Console.WriteLine($"  Order #{e.OrderId} {status}: ${e.Amount}");
    Console.WriteLine($"  Processed at: {e.ProcessedAt:HH:mm:ss}");
};

orderService.ProcessOrder(1001, 299.99m);

Events with Custom Accessors (add/remove)

public class ButtonManager
{
    private EventHandler? _clicked;

    public event EventHandler? Clicked
    {
        add
        {
            Console.WriteLine($"Subscriber added: {value.Method.Name}");
            _clicked += value;
        }
        remove
        {
            Console.WriteLine($"Subscriber removed: {value.Method.Name}");
            _clicked -= value;
        }
    }

    public void SimulateClick()
    {
        _clicked?.Invoke(this, EventArgs.Empty);
    }
}

Delegates as Parameters

public class DataProcessor
{
    public List<TResult> Process<T, TResult>(
        IEnumerable<T> data,
        Func<T, bool> filter,
        Func<T, TResult> transformer)
    {
        return data
            .Where(filter)
            .Select(transformer)
            .ToList();
    }
}

var processor = new DataProcessor();
var numbers = Enumerable.Range(1, 20);

var result = processor.Process(
    numbers,
    filter: n => n % 2 == 0,        // Keep only even
    transformer: n => $"Number: {n} * 2 = {n * 2}"
);

foreach (var item in result)
    Console.WriteLine(item);

Async Event Handlers

public class AsyncEventExample
{
    public event EventHandler<AsyncEventArgs>? DataProcessed;

    public async Task ProcessDataAsync()
    {
        Console.WriteLine("Processing data...");
        await Task.Delay(100);

        // Fire-and-forget for async event handlers
        var handlers = DataProcessed?.GetInvocationList();
        if (handlers != null)
        {
            var args = new AsyncEventArgs();
            foreach (EventHandler<AsyncEventArgs> handler in handlers)
            {
                if (handler.Target is IAsyncEventHandler asyncHandler)
                    await asyncHandler.HandleAsync(this, args);
                else
                    handler(this, args);
            }
        }
    }
}

Common Mistakes

Mistake 1: Forgetting to Check for Null Before Invoking

event?.Invoke(this, args); is the safe pattern. Without the null check, if there are no subscribers, the event is null and causes a NullReferenceException.

Mistake 2: Using Public Delegate Fields Instead of Events

Public delegate fields allow external code to invoke the delegate directly. Events restrict invocation to the declaring class. Always use events for public notification points.

Mistake 3: Not Unsubscribing Events (Memory Leaks)

Subscribed event handlers prevent the subscriber from being garbage collected. Always unsubscribe when the subscriber is disposed:

button.Clicked -= ButtonHandler;

Mistake 4: Raising Events Outside the Declaring Class

Events can only be raised from within the class that declares them. This is intentional Encapsulation. Use a protected virtual method for derived class access.

Mistake 5: Assuming Event Handler Execution Order

The order in which multicast delegates execute is not guaranteed by the specification. Do not rely on subscription order for correctness.

Mistake 6: Using += Without Checking for Duplicates

+= adds the handler even if it is already subscribed. Each unsubscription removes only one instance. Use the same delegate instance for both add and remove.

Practice Questions

  1. What is the difference between a delegate and an event?
  2. How does multicast delegation work?
  3. Why does the EventHandler pattern use sender and EventArgs parameters?
  4. When would you use Func<T, TResult> instead of a custom delegate type?
  5. Write a TemperatureMonitor class that raises an event when the temperature exceeds a threshold.

Challenge

Create a simple publish-subscribe event bus that allows any type of event to be published and any subscriber to listen for specific event types. Use generics and the standard EventHandler pattern.

FAQ

What is the difference between `event` and `delegate`?

An event is a wrapper around a delegate that restricts external access. Subscribers can only add/remove handlers via += and -=. The declaring class is the only one that can invoke the event.

Are delegates type-safe?

Yes. Delegates are type-safe. The signature of the delegate must exactly match the method or lambda assigned to it. The compiler enforces this at compile time.

What is the EventHandler pattern?

It is a standardized pattern where events use EventHandler<TEventArgs> with a sender (object) and e (TEventArgs) parameter. TEventArgs inherits from EventArgs. This is the convention used throughout the .NET Framework.

Can events be async?

Event handlers can be async (return Task), but events themselves cannot. Fire multiple async handlers with Task.WhenAll. Be careful with exception handling in async void handlers.

What is the difference between `Action` and `Func`?

Action delegates return void. Func delegates return a value (specified by the last type parameter). Action has 0-16 parameters. Func has 1-16 parameters plus a return type.

Mini Project

Create a stock price monitoring system:

public class StockPriceChangedEventArgs : EventArgs
{
    public string Symbol { get; init; }
    public decimal OldPrice { get; init; }
    public decimal NewPrice { get; init; }
    public decimal ChangePercent => (NewPrice - OldPrice) / OldPrice * 100;
}

public class Stock
{
    private decimal _price;
    private static readonly Random _random = new();

    public string Symbol { get; }
    public decimal Price => _price;

    public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;

    public Stock(string symbol, decimal initialPrice)
    {
        Symbol = symbol;
        _price = initialPrice;
    }

    public void UpdatePrice(decimal newPrice)
    {
        var oldPrice = _price;
        _price = newPrice;

        PriceChanged?.Invoke(this, new StockPriceChangedEventArgs
        {
            Symbol = Symbol,
            OldPrice = oldPrice,
            NewPrice = newPrice
        });
    }

    public void SimulateDay()
    {
        var change = _price * (decimal)(_random.NextDouble() * 0.1 - 0.05);
        UpdatePrice(_price + change);
    }
}

public class StockTrader
{
    public string Name { get; }
    public StockTrader(string name) => Name = name;

    public void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
    {
        if (e.ChangePercent > 3)
            Console.WriteLine($"  {Name}: ALERT! {e.Symbol} up {e.ChangePercent:F2}%");
        else if (e.ChangePercent < -3)
            Console.WriteLine($"  {Name}: ALERT! {e.Symbol} down {e.ChangePercent:F2}%");
    }
}

var msft = new Stock("MSFT", 400.00m);
var trader1 = new StockTrader("Alice");
var trader2 = new StockTrader("Bob");

msft.PriceChanged += trader1.OnPriceChanged;
msft.PriceChanged += trader2.OnPriceChanged;

Console.WriteLine("Stock Price Changes:");
for (int day = 1; day <= 5; day++)
{
    Console.WriteLine($"\nDay {day}:");
    msft.SimulateDay();
    Console.WriteLine($"  Current price: ${msft.Price:F2}");
}

Expected output (varies):

Stock Price Changes:

Day 1:
  Alice: ALERT! MSFT down -3.50%
  Bob: ALERT! MSFT down -3.50%
  Current price: $386.00

Day 2:
  Current price: $389.86

Day 3:
  Alice: ALERT! MSFT up 4.12%
  Bob: ALERT! MSFT up 4.12%
  Current price: $405.92

Day 4:
  Current price: $406.35

Day 5:
  Current price: $401.47

What's Next

You have mastered delegates and events in C#. The next lesson covers lambdas: lambda syntax, closures, expression trees, and Func/Action/Predicate usage.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro