Skip to main content

C# Delegates, Events and Func/Action: Complete Guide

Learn C# delegates, events, and Func/Action with practical examples. Master event-driven programming in C# the right way — 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 is C# delegates. Delegates, events, and the built-in Func and Action types form the backbone of event-driven programming in .NET. They power everything from UI frameworks like WPF and ASP.NET Core to LINQ, async callbacks, and the observer pattern.

In this complete guide, you'll learn exactly what C# delegates are, how they relate to events and the Func/Action family, and—most importantly—why they exist. We'll cover practical, runnable code examples, best practices, and the common pitfalls that trip up beginners and intermediates alike. Whether you're searching for a simple C# delegate example or trying to understand delegate vs event, this article has you covered.

What Are C# Delegates? (The Foundation)

A delegate is a type-safe function pointer. Put simply, it's a variable that holds a reference to a method instead of holding data. Just as an int can hold a number, a delegate can hold a method—and you can pass that method around, store it, and invoke it later.

Why does this matter? Because it lets you treat behavior as data. Instead of hard-coding what happens next, you can pass in which method to call, enabling flexible, decoupled, and reusable code. This is the core idea behind event-driven programming in C#.

Here's a basic C# delegate example:

// 1. Declare a delegate type — defines the method signature it can hold
public delegate int MathOperation(int a, int b);

public class Calculator
{
    public static int Add(int a, int b) => a + b;
    public static int Multiply(int a, int b) => a * b;

    public static void Main()
    {
        // 2. Assign a method to the delegate
        MathOperation op = Add;
        Console.WriteLine(op(3, 4)); // Output: 7

        // 3. Point it at a different method — behavior changes, code doesn't
        op = Multiply;
        Console.WriteLine(op(3, 4)); // Output: 12
    }
}

Notice how op holds different methods at different times. The calling code—op(3, 4)—never changes, but the behavior does. That's the power of delegates.

Multicast Delegates: Calling Many Methods at Once

Delegates in C# are multicast, meaning a single delegate instance can reference multiple methods. You combine them with += and remove them with -=. When invoked, all attached methods run in order.

public delegate void Notify(string message);

public class Logger
{
    public static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
    public static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");

    public static void Main()
    {
        Notify notifier = LogToConsole;
        notifier += LogToFile; // Now both methods are attached

        notifier("System started");
        // Output:
        // Console: System started
        // File: System started
    }
}

This multicast behavior is exactly what makes the publish-subscribe pattern—and therefore events—possible.

Func and Action: The Built-In Delegates You Should Actually Use

In modern C#, you rarely need to declare your own delegate types. The .NET framework provides two generic delegate families that cover almost every case: Func and Action in C#.

  • Action — represents a method that returns void. Action<T> takes parameters but returns nothing.
  • Func — represents a method that returns a value. The last type parameter is always the return type. So Func<int, int, int> takes two ints and returns an int.
  • Predicate — a specialized Func<T, bool> used for tests and filtering.
// Action: takes input, returns nothing
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice"); // Hello, Alice!

// Func: takes two ints, returns an int (last param is the return type)
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(5, 7)); // 12

// Predicate: returns bool — perfect for filtering
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(10)); // True

Why prefer Func and Action over custom delegates? Because they're instantly recognizable to every C# developer, they reduce boilerplate, and they integrate seamlessly with LINQ and the entire .NET ecosystem. When you call list.Where(x => x > 5), you're passing a Func<int, bool> behind the scenes.

Lambda Expressions and Anonymous Methods

The => syntax above is a lambda expression—a concise, inline way to define a method without naming it. Before lambdas, C# used anonymous methods with the delegate keyword. You'll still see both in older codebases:

// Modern lambda (preferred)
Func<int, int> square = x => x * x;

// Older anonymous method syntax (equivalent)
Func<int, int> squareOld = delegate (int x) { return x * x; };

Lambdas can also capture variables from their surrounding scope—a feature called a closure. This is powerful but also a common source of bugs, which we'll discuss in the pitfalls section.

C# Events: Delegates with Guardrails

An event is a delegate with restricted access. It's the standard mechanism for the publisher-subscriber (observer) pattern: one class (the publisher) raises a notification, and any number of other classes (subscribers) react to it—without the publisher knowing or caring who's listening.

So what's the real delegate vs event difference? A plain public delegate can be reassigned (=), invoked, or even cleared by anyone holding a reference. An event only allows external code to subscribe (+=) and unsubscribe (-=). Only the declaring class can raise (invoke) it. This encapsulation prevents subscribers from accidentally wiping out each other's handlers or firing the event themselves.

Here's the standard .NET event pattern using EventHandler<T>:

// Custom event data
public class OrderPlacedEventArgs : EventArgs
{
    public string ProductName { get; }
    public decimal Amount { get; }

    public OrderPlacedEventArgs(string productName, decimal amount)
    {
        ProductName = productName;
        Amount = amount;
    }
}

// The publisher
public class OrderService
{
    // Declare the event using the recommended EventHandler delegate
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

    public void PlaceOrder(string product, decimal amount)
    {
        Console.WriteLine($"Processing order for {product}...");

        // Raise the event safely with the null-conditional operator
        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(product, amount));
    }
}

// The subscribers
public class Program
{
    public static void Main()
    {
        var service = new OrderService();

        // Subscribe: send a confirmation email
        service.OrderPlaced += (sender, e) =>
            Console.WriteLine($"Email sent: Thanks for buying {e.ProductName}!");

        // Subscribe: update inventory
        service.OrderPlaced += (sender, e) =>
            Console.WriteLine($"Inventory updated for {e.ProductName} (${e.Amount})");

        service.PlaceOrder("Mechanical Keyboard", 89.99m);
    }
}

When PlaceOrder runs, both subscribers fire automatically. The OrderService has zero knowledge of email or inventory logic. You could add SMS notifications, analytics, or logging later—without touching OrderService at all. This loose coupling is why event-driven programming in C# scales so well in large applications.

Why Use EventHandler<T> and ?.Invoke?

Two best practices appear in the code above:

  • EventHandler<T> follows the .NET convention of (object sender, TEventArgs e). Tooling, documentation, and other developers all expect this signature—stick to it.
  • OrderPlaced?.Invoke(...) uses the null-conditional operator. If no one has subscribed, the event is null, and invoking it directly would throw a NullReferenceException. The ?. check is the safest, thread-conscious way to raise an event.

Practical Use Cases for Delegates and Events

Understanding the theory is one thing; knowing when to reach for these tools is another. Here are the most common real-world scenarios:

  • Callbacks: Pass an Action to run after an async operation completes.
  • LINQ queries: Where, Select, and OrderBy all take Func delegates.
  • UI events: Button clicks, text changes, and window resizing in WPF/WinForms.
  • Strategy pattern: Inject different algorithms at runtime via a delegate parameter.
  • Decoupled notifications: Domain events in clean architecture and DDD.

Here's a strategy-pattern example showing how a delegate parameter makes a method endlessly flexible:

public class DataProcessor
{
    // Accept ANY transformation as a Func parameter
    public List<int> Process(List<int> data, Func<int, int> transform)
    {
        var result = new List<int>();
        foreach (var item in data)
            result.Add(transform(item));
        return result;
    }
}

// Usage — one method, infinite behaviors
var processor = new DataProcessor();
var numbers = new List<int> { 1, 2, 3, 4 };

var doubled = processor.Process(numbers, x => x * 2);   // 2, 4, 6, 8
var squared = processor.Process(numbers, x => x * x);   // 1, 4, 9, 16

Common Pitfalls and Best Practices

Delegates and events are elegant, but a few traps catch even experienced developers. Here are the ones worth memorizing.

1. Memory Leaks from Forgotten Unsubscriptions

This is the single most common delegate-related bug. When a subscriber subscribes to an event, the publisher holds a reference to it. If the subscriber is never unsubscribed, the garbage collector can't reclaim it—causing a memory leak that grows over the application's lifetime.

// ALWAYS unsubscribe when done, especially for long-lived publishers
service.OrderPlaced += handler;
// ... later, when the subscriber is no longer needed:
service.OrderPlaced -= handler;

Important: you can only unsubscribe a handler if you keep a reference to it. Anonymous lambdas can't be removed because you don't have a handle to them. Store the lambda in a variable if you'll need to detach it later.

2. The Closure Loop Variable Trap

When you capture a loop variable in a lambda, classic C# closures captured the variable, not its value. (C# 5+ fixed this for foreach, but the gotcha still appears in for loops.)

var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    int copy = i; // Capture a local copy to avoid the trap
    actions.Add(() => Console.WriteLine(copy));
}
actions.ForEach(a => a()); // Prints 0, 1, 2 (not 3, 3, 3)

3. Always Null-Check Before Raising Events

As shown earlier, use MyEvent?.Invoke(this, args). Never invoke an event without the null-conditional operator unless you've guaranteed at least one subscriber.

4. Don't Over-Engineer with Custom Delegates

If Func, Action, or EventHandler<T> fits, use it. Reserve custom delegate declarations for cases where a descriptive name genuinely improves readability or when you need ref/out parameters (which Func/Action don't support).

5. Beware Exceptions in Multicast Invocations

If one subscriber in a multicast delegate throws, the remaining handlers won't run. For critical notifications, consider invoking handlers individually via GetInvocationList() and wrapping each in a try-catch.

Conclusion: Key Takeaways on C# Delegates and Events

Mastering C# delegates, events, and the Func/Action family is a milestone that separates beginner C# developers from confident, professional ones. These features are the foundation of event-driven programming in C#, LINQ, async callbacks, and clean, decoupled architecture.

Here's what to remember:

  • Delegates are type-safe references to methods—they let you treat behavior as data.
  • Func and Action are the built-in delegates you should use 95% of the time. Func returns a value; Action returns void.
  • Events are delegates with encapsulation—they enable the publisher-subscriber pattern safely. This is the heart of the delegate vs event distinction.
  • Always raise events with ?.Invoke(), follow the EventHandler<T> convention, and unsubscribe to prevent memory leaks.
  • Watch out for closure traps and exceptions in multicast delegates.

The best way to internalize these concepts is to use them. Try refactoring a tightly coupled class in your own project to raise an event instead, or replace a hard-coded method call with a Func parameter. Once event-driven thinking clicks, you'll write more flexible, testable, and maintainable C# code. 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...