Mastering Delegates in C#

Patrick KearnsPatrick Kearns
4 min read

Delegates in C# are much more than just function pointers, they are foundational to many of the language’s most powerful features, including events, asynchronous programming, and LINQ. Understanding how to use delegates properly opens the door to writing flexible, decoupled, and testable code.

What Are Delegates?

A delegate is a type that represents references to methods with a particular parameter list and return type. Think of it as a contract that any matching method can satisfy.

delegate int MathOperation(int a, int b);

This defines a delegate that can point to any method that takes two int parameters and returns an int.

Example usage:

Add(int x, int y) => x + y;
MathOperation op = Add;
Console.WriteLine(op(3, 4)); // 7

Why Use Delegates?

  • Decoupling logic

  • Flexibility in passing methods as parameters

  • Enabling extensibility (e.g., callbacks, strategies)

  • Core to events and LINQ

  • Better unit testing with mockable logic. Multicast Delegates

Delegates can point to multiple methods using the += and -= operators.

notify = () => Console.WriteLine("First");
notify += () => Console.WriteLine("Second");
notify();

Return values from multicast delegates are only from the last invocation.

Built In Generic Delegates: Func, Action, Predicate

Action

No return value:

Action<string> log = message => Console.WriteLine($"Log: {message}");
log("Test");

Func

Returns a value:

Func<int, int, int> add = (a, b) => a + b;
int result = add(2, 3); // 5

Predicate

Returns bool:

Predicate<string> isEmpty = s => string.IsNullOrEmpty(s);
bool check = isEmpty("");

These are often more concise than custom delegate types.

Delegates as Strategy Pattern

Instead of tightly coupling logic:

public class PaymentProcessor
{
    public void Process(Payment payment) { /* logic */ }
}

Use delegates for pluggability:

public class PaymentProcessor
{
    private readonly Action<Payment> _strategy;

    public PaymentProcessor(Action<Payment> strategy)
    {
        _strategy = strategy;
    }

    public void Process(Payment payment)
    {
        _strategy(payment);
    }
}

This makes the behaviour easily swappable, mockable, and testable.

Higher Order Functions and Currying

A higher order function takes a delegate or returns one.

Func<int, Func<int, int>> add = x => y => x + y;
var addFive = add(5);
Console.WriteLine(addFive(10)); // 15

Currying like this is rarely used in C# day to day but can unlock powerful abstractions in complex business rules or pipelines.

Delegates and Events

Events are essentially multicast delegates with restrictions.

public class Alarm
{
    public event Action? OnRing;

    public void Ring()
    {
        OnRing?.Invoke();
    }
}

Events let subscribers register methods:

var alarm = new Alarm();
alarm.OnRing += () => Console.WriteLine("Wake up!");
alarm.Ring();

Always check for null or use the null conditional operator (?.) when invoking events.

Asynchronous Delegates

Delegates can be called asynchronously with BeginInvoke / EndInvoke, but in modern C#, use async and Task instead.

Func<int, int, Task<int>> addAsync = async (a, b) =>
{
    await Task.Delay(100);
    return a + b;
};

var result = await addAsync(3, 4);

Or use delegates to orchestrate async pipelines:

Func<HttpRequestMessage, Task<HttpResponseMessage>> pipeline = async request =>
{
    // logging, auth, etc.
    return await httpClient.SendAsync(request);
};

Delegates in LINQ

LINQ heavily uses delegates — both Func<T, bool> and Func<T, TResult>.

var names = new[] { "alice", "bob", "carol" };
var upper = names.Select(name => name.ToUpper()); // Func<string, string>

This allows for expressive and composable data transformation pipelines.

Real World Example: Retry Logic

You can write reusable retry logic with delegates:

public static T Retry<T>(Func<T> operation, int maxAttempts = 3)
{
    int attempts = 0;
    while (true)
    {
        try
        {
            return operation();
        }
        catch
        {
            if (++attempts >= maxAttempts)
                throw;
        }
    }
}

Usage:

int result = Retry(() => UnreliableOperation());

Unit Testing with Delegates

Using delegates instead of tightly coupled code makes testing easier.

Example:

public class EmailSender
{
    private readonly Func<string, string, bool> _sendFunc;

    public EmailSender(Func<string, string, bool> sendFunc)
    {
        _sendFunc = sendFunc;
    }

    public bool Send(string subject, string body)
    {
        return _sendFunc(subject, body);
    }
}

In a unit test:

var sender = new EmailSender((s, b) => s.Contains("Test"));
Assert.True(sender.Send("Test subject", "Hello"));

Combining Delegates Dynamically

You can build pipelines by composing delegates:

Func<string, string> step1 = s => s.ToUpper();
Func<string, string> step2 = s => $"Hello, {s}";
Func<string, string> pipeline = s => step2(step1(s));

var result = pipeline("patrick"); // "Hello, PATRICK"

This works well in middleware style architectures.

Custom Delegate Types vs Func/Action

When to define your own delegate type:

  • You want semantic clarity in public APIs.

  • You want to use attributes (e.g., [Obsolete]) on delegate parameters.

  • You need to support multicast scenarios with event like semantics.

Otherwise, Func<> and Action<> are sufficient for internal use and concise code.


Delegates are the backbone of flexible and composable C# applications. Mastering them enables clean separation of concerns, extensibility without inheritance & easy testability through injection. You also get functional patterns like pipelines and transformations that promote clean decoupled code.

While many developers use delegates indirectly via LINQ or events without thinking about it, going deeper into custom delegate usage, higher order functions, and strategies will improve your design skills in large systems.

0
Subscribe to my newsletter

Read articles from Patrick Kearns directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Patrick Kearns
Patrick Kearns