Skip to main content

C# Delegates and Events Explained (With Code Examples)

Learn C# delegates and events with practical, runnable code examples. Master event-driven programming, best practices, and common pitfalls. Start coding today!

If you've ever wondered how a button "knows" when it's clicked, or how one part of your application can notify another without being tightly coupled to it, the answer lies in C# delegates and events. Understanding delegates and events is one of the most important steps in moving from beginner to professional .NET developer, because they power everything from UI frameworks and async callbacks to the publish-subscribe patterns used in enterprise software. In this tutorial, we'll explain C# delegates and events from the ground up, with practical, runnable code examples and a complete event-driven mini-application.

By the end of this guide you'll know exactly what a delegate is, how it relates to events, when to use built-in types like Action and Func, and the best practices that keep your event-driven code safe and maintainable.

What Are Delegates in C#?

A delegate in C# is a type that holds a reference to a method. Think of it as a strongly-typed function pointer: it defines a method signature (return type and parameters), and any method matching that signature can be assigned to it. This lets you pass methods around as arguments, store them in variables, and invoke them dynamically at runtime.

Why does this matter? Delegates enable decoupling. Instead of calling a method directly, you call it through a delegate, which means the caller doesn't need to know which concrete method will run. This is the foundation of event-driven programming in C#.

using System;

public class Program
{
    // 1. Declare a delegate type that takes a string and returns void
    public delegate void Notify(string message);

    public static void Main()
    {
        // 2. Point the delegate at a real method
        Notify notifier = SendEmail;

        // 3. Invoke the method through the delegate
        notifier("Order #1024 has shipped!");

        // You can reassign it to a different method with the same signature
        notifier = SendSms;
        notifier("Order #1024 has shipped!");
    }

    static void SendEmail(string message) => Console.WriteLine($"EMAIL: {message}");
    static void SendSms(string message) => Console.WriteLine($"SMS: {message}");
}

Notice how notifier can call SendEmail or SendSms without the Main method caring which one runs. That flexibility is the whole point of a delegate.

Multicast Delegates: Calling Many Methods at Once

One powerful feature of delegates is that they are multicast, meaning a single delegate instance can reference multiple methods. Use the += operator to add methods and -= to remove them. When invoked, all subscribed methods run in the order they were added.

Notify notifier = SendEmail;
notifier += SendSms;   // now both run
notifier += LogToConsole;

notifier("Payment received");
// Output:
// EMAIL: Payment received
// SMS: Payment received
// LOG: Payment received

This multicast behavior is exactly what makes the C# event model work — a single notification can reach many listeners.

Action, Func, and Predicate: Built-In Delegates

In modern C# you rarely need to declare your own delegate types. The .NET base class library provides generic built-in delegates that cover almost every scenario, and using them makes your code more idiomatic and readable:

  • Action — references a method that returns void. Action<string> takes a string and returns nothing.
  • Func — references a method that returns a value. The last generic argument is the return type, e.g. Func<int, int, int> takes two ints and returns an int.
  • Predicate — references a method that returns a bool, typically used for filtering, e.g. Predicate<int>.
Action<string> log = msg => Console.WriteLine(msg);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;

log("Using built-in delegates");
Console.WriteLine(add(3, 4));      // 7
Console.WriteLine(isEven(10));     // True

The lambda expressions (=>) above are simply concise, inline methods assigned to the delegates. If you've used LINQ, you've already been using delegates without realizing it.

What Are Events in C#?

An event in C# is a special wrapper around a delegate that enforces the publish-subscribe pattern. It allows a class (the publisher) to notify other classes (the subscribers) when something happens, while protecting the underlying delegate from misuse.

The key difference between a plain delegate and an event: with a public delegate field, any outside code could overwrite all subscribers with =, or even invoke the delegate itself. An event restricts outside code to only += (subscribe) and -= (unsubscribe). Only the declaring class can raise (invoke) the event. This encapsulation is why events are the recommended way to implement notifications in C#.

A Practical Event-Driven Example in C#

Let's build something realistic: a stock price monitor. When a stock's price changes, the system should automatically notify multiple subscribers — a dashboard, an alert service, and a logger — without the stock class knowing anything about them. This is event-driven programming in C# in action.

The .NET convention is to use the EventHandler<TEventArgs> delegate and a custom EventArgs class to carry event data.

using System;

// 1. Custom event data, by convention inherits from EventArgs
public class PriceChangedEventArgs : EventArgs
{
    public string Symbol { get; }
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }

    public PriceChangedEventArgs(string symbol, decimal oldPrice, decimal newPrice)
    {
        Symbol = symbol;
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}

// 2. The publisher
public class Stock
{
    private decimal _price;
    public string Symbol { get; }

    // The event, based on the generic EventHandler delegate
    public event EventHandler<PriceChangedEventArgs> PriceChanged;

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

    public decimal Price
    {
        get => _price;
        set
        {
            if (_price == value) return;
            var oldPrice = _price;
            _price = value;
            // Raise the event safely
            OnPriceChanged(new PriceChangedEventArgs(Symbol, oldPrice, value));
        }
    }

    // 3. Protected virtual method to raise the event (the standard pattern)
    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        // Null-conditional operator prevents a crash if there are no subscribers
        PriceChanged?.Invoke(this, e);
    }
}

Now the subscribers. Each one reacts to the same event independently:

public class Program
{
    public static void Main()
    {
        var apple = new Stock("AAPL", 190.00m);

        // Subscribe multiple handlers to the same event
        apple.PriceChanged += Dashboard_Update;
        apple.PriceChanged += AlertService_Check;
        apple.PriceChanged += (sender, e) =>
            Console.WriteLine($"LOG: {e.Symbol} moved {e.OldPrice} -> {e.NewPrice}");

        // Changing the price automatically notifies every subscriber
        apple.Price = 195.50m;
        apple.Price = 188.25m;

        // Unsubscribe when no longer needed
        apple.PriceChanged -= Dashboard_Update;
    }

    static void Dashboard_Update(object sender, PriceChangedEventArgs e)
        => Console.WriteLine($"DASHBOARD: {e.Symbol} is now ${e.NewPrice}");

    static void AlertService_Check(object sender, PriceChangedEventArgs e)
    {
        var change = (e.NewPrice - e.OldPrice) / e.OldPrice * 100;
        if (Math.Abs(change) > 2)
            Console.WriteLine($"ALERT: {e.Symbol} moved {change:F2}%!");
    }
}

Run this and a single apple.Price = 195.50m assignment triggers the dashboard, the alert service, and the logger — all without the Stock class containing a single reference to any of them. That is the power of events: loose coupling. You can add or remove subscribers at any time without touching the publisher.

Delegates vs Events: What's the Difference?

This is one of the most common interview questions about C# delegates and events, so let's make it crystal clear:

  • A delegate is a type that holds method references. It can be invoked and reassigned by anyone who can access it.
  • An event is a layer of encapsulation built on top of a delegate. Outside code can only subscribe (+=) or unsubscribe (-=); only the owning class can raise it.

In short: every event uses a delegate under the hood, but not every delegate is an event. Use a plain delegate when you need to pass a method as a parameter (like a callback or a LINQ filter). Use an event when you're modeling notifications that external code should react to but not control.

Best Practices for C# Delegates and Events

  • Always use the null-conditional operator when raising events: PriceChanged?.Invoke(this, e). This avoids a NullReferenceException when there are no subscribers.
  • Follow the standard event pattern: use EventHandler<TEventArgs>, a custom EventArgs subclass, and a protected virtual OnXxx method so derived classes can override behavior.
  • Prefer Action and Func over custom delegate types for callbacks. They're instantly recognizable to other developers.
  • Unsubscribe to prevent memory leaks. A subscriber that never unsubscribes keeps the publisher alive, and vice versa — a frequent cause of leaks in long-lived applications.
  • Keep event handlers fast and exception-safe. Because handlers run synchronously in sequence, one slow or throwing handler can block or break the rest.

Common Pitfalls to Avoid

  • Exceptions in a handler stop the chain. If the second of three subscribers throws, the third never runs. For critical scenarios, invoke handlers individually using GetInvocationList() and wrap each in a try-catch.
  • The lambda unsubscribe trap. You cannot unsubscribe an anonymous lambda with -= because you don't hold a reference to it. Store it in a variable first if you'll need to detach it.
  • Race conditions in multithreaded code. Copy the event to a local variable before checking and invoking, or rely on the null-conditional operator which does this for you.
  • Forgetting that delegates are immutable. += and -= create new delegate instances rather than mutating the existing one — important to understand when reasoning about threading.

Conclusion: Key Takeaways

Mastering C# delegates and events unlocks a whole category of clean, decoupled, event-driven designs that scale well as your applications grow. Here are the key takeaways:

  • A delegate is a strongly-typed reference to a method, enabling you to pass behavior around as data.
  • Multicast delegates let one call invoke many methods, which is the engine behind events.
  • Use built-in Action, Func, and Predicate instead of declaring custom delegate types in most cases.
  • An event is an encapsulated delegate that safely implements the publish-subscribe pattern — the standard way to do notifications in .NET.
  • Follow the EventHandler<T> + EventArgs + OnXxx pattern, always null-check before raising, and unsubscribe to avoid memory leaks.

Now that you understand how delegates and events work together, try extending the stock monitor example: add a handler that logs to a file, or build your own event-driven notification system. Hands-on practice is the fastest way to make event-driven programming in C# second nature. Happy coding!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...