Mastering C# Delegates: The Remote Controls That Power Your Code by Arpit Shukla on May 18, 2026 15 views

You must have crossed the bridge where you might need your code to execute a task for you, but the task is decided at runtime. So you write a list of if-else statements or maybe a switch block. Boom! Problem solved. Yes, You solved the problem.

And then… the client calls. They want changes. You dive back in, only to find yourself tangled in a messy web of conditional statements.

But what if I told you, there’s a more flexible and robust way to manage this dynamic behavior? That’s where delegates come in.

Delegates provide a powerful way to achieve flexible and reusable code by allowing you to treat methods as objects, ready to be passed around and executed on demand.

Delegates are data structures that point to one or more methods. They are similar to function pointers in C, C++, but with greater capabilities.

They don’t just store the method’s reference, but also a reference to the object instance on which to invoke the method.

💡 In simple terms, a delegate is a reference to a method, allowing methods to be assigned to variables and passed around like data.

It’s a type-safe way to call methods dynamically in C#.

Why delegates are so powerful:

  • Can encapsulate both static and instance methods.
  • Can store references to multiple methods(multicasting)
  • Events are made on top of delegates.
  • Delegates support Covariance and Contravariance
  • Lambda expressions (in certain contexts) are compiled to delegate types.(So no need to write lengthy code)

How Delegates Work:

When a delegate is declared, it defines a template (or signature) for the methods it can reference.

Any method that matches this signature can be assigned to the delegate.

public delegate void MessageDelegate(string param);

Oh! It looks pretty much the same as a function declaration.

Yes, exactly — the only difference is the delegate keyword, which tells the compiler that this is not a method but a type that can hold a reference to any method matching its signature.

Now, even though the delegate and the method have different names, what matters is that the method we assign to the delegate must match the signature (i.e., same return type and parameters) — not the name.

Think of the delegate as a contract: if a method fulfills that contract, it can be assigned to the delegate, regardless of what the method is named.

This allows you to create reusable logic pipelines, pass behavior dynamically, and decouple your code in clean and powerful ways.

So, let’s define a function:

// Define a method that matches the delegate signature
public class Messenger
{
    public void Show(string msg)
    {
        Console.WriteLine("Message: " + msg);
    }
}

Now, we have a function whose definition matches with our delegate, so now we will pass this function to our delegate and then call the delegate instead of calling the function directly.

// Use the delegate
class Program
{
    static void Main()
    {
        Messenger messenger = new Messenger();

        // Assign method to delegate
        MessageDelegate messageShow = messenger.Show;

        // Invoke delegate
        messageShow("Hello, world!"); // Output: Message: Hello, world!
    }
}

It gives the same result and functionality-wise it is the same as calling the function directly, but now we have the flexibility to write the code and make modifications in the future.

  • Now we can change the method being called at runtime.
  • We can pass the delegate to other classes or methods, like any other variable.
  • We can combine multiple methods into one delegate(Multicast Delegates).

Why do we need to use the Delegates?

On first look, delegates seem like a waste of time. Why do we have to wrap our method in another type to just call it. But as the application grows in complexity, using delegates starts to make sense.

We have seen some use cases already like:

  • Flexibility at runtime
  • Cleaner and Reusable code

Let’s see the other in detail too.

Pass Behavior around

Delegates give you the ability to pass a method as a parameter, essentially allowing you to hand off a specific task to another piece of code.

Think of it as telling a method,

“Here’s what you need to do, but I’ll let you know the exact action to perform later.”

This is a perfect solution for problems like filtering a collection where the filtering logic might change. By passing the filter method as a delegate, you eliminate the need for redundant if-else statements and create a design that is both highly reusable and simple to extend.

Foundation Of Events:

Delegates are also the building blocks of Events. In C#, events rely on delegates to define the contract for the methods that can “handle” an event. This is how we can easily subscribe to notifications and build decoupled systems where objects can communicate without being tightly bound to each other.

Useful in implementing Design Patterns:

So far, we’ve seen that delegates are pretty powerful. And if you look closely, you’ll realise these core features of delegates are nothing but the fundamental principle of a good design pattern.

So, let’s see examples of the two patterns which can be easily implemented using delegates.

The Strategy Pattern and the Observable Pattern.

Strategy Pattern:

The strategy design pattern is one of the behavioral design patterns. It is used when we have multiple algorithms for a specific task, and the client decides the actual implementation to be used at runtime.

Hmmm! This sounds somewhat familiar, doesn’t it?

Yup — the first few use cases of delegates we covered (passing behavior, filtering, runtime decisions) map directly to this!

Although you may implement the strategy pattern using interfaces or abstract classes. But with delegates in our toolbox, we do not need to create multiple classes and interfaces.

You can skip the boilerplate.

Just pass the behavior as a method or a lambda.

Let’s try to understand the above with a simple example.

If we were to use the traditional approach, we would have to create different classes for different strategies.

public interface IStrategy
{
    int Execute(int a, int b);
}

public class AddStrategy : IStrategy
{
    public int Execute(int a, int b) => a + b;
}

public class MultiplyStrategy : IStrategy
{
    public int Execute(int a, int b) => a * b;
}

public class Calculator
{
    private IStrategy _strategy;

    public Calculator(IStrategy strategy)
    {
        _strategy = strategy;
    }

    public int Calculate(int a, int b)
    {
        return _strategy.Execute(a, b);
    }
}

// Usage
var calculator = new Calculator(new MultiplyStrategy());
int result = calculator.Calculate(4, 5); // Output: 20

And now let’s implement the same using delegates.

// Delegate definition
public delegate int Operation(int a, int b);

public class Calculator
{
    public int Calculate(int a, int b, Operation op)
    {
        return op(a, b);
    }
}

// Usage
var calc = new Calculator();

int result = calc.Calculate(4, 5, (x, y) => x * y); // Output: 20

// Here we have passed a lambda expression as 'Operation'. 
// We can easily pass any lambda expression that returns an integer, 
// as we have defined Operation as a delegate 
// that takes two integers as input and returns an integer as a result.

In the traditional Strategy Pattern, every new behavior requires creating a new class that implements the IStrategy interface. For example, if you want to add SubtractStrategy or DivideStrategy, you’ll need to define new classes. This leads to:

  • Extra boilerplate code (class definitions, constructors, interfaces).
  • Code sprawl — many small classes that only contain one line of logic.

On the other hand, when we use delegates, we can directly pass the behavior (method or lambda expression) as a parameter:

int result1 = calc.Calculate(4, 5, (x, y) => x + y);  // Addition
int result2 = calc.Calculate(4, 5, (x, y) => x - y);  // Subtraction
int result3 = calc.Calculate(4, 5, (x, y) => x * y);  // Multiplication

This way:

  • Reusability: The same Calculate method can work with any operation without needing extra classes.
  • Readability: The operation’s intent (+, , , etc.) is visible right where it’s used.
  • Less Code: No need for multiple small strategy classes — just pass the logic directly.

Now, let’s also look into the observer pattern.

The Observer pattern is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects of any events that occur on the object they’re observing.

Sounds like event handling. Right?

Bingo! You got it.

Observer pattern is deeply rooted in event handling and management.

In C#, the observer pattern is deeply tied to events, and events are based on multicast delegates.

Do not worry about the multicast delegates. We will look into them in just a second. But first, let us see how we can implement the observer pattern.

You might have noticed the use of the += operator when subscribing to an event. That’s not just for adding a single method — it’s actually how we attach multiple methods to the same delegate. And that’s exactly what leads us into the concept of multicast delegates.

Multicast Delegates:

Multicast delegates are delegates which contain references to multiple methods.

When a multicast delegate is invoked, all the methods which are referenced by the delegate are invoked. The methods are invoked in the order they are added.

public delegate void Notify();  // Delegate with no parameters

public class NotificationService
{
    public void Email()
    {
        Console.WriteLine("Email sent.");
    }

    public void SMS()
    {
        Console.WriteLine("SMS sent.");
    }

    public void PushNotification()
    {
        Console.WriteLine("Push Notification sent.");
    }
}

class Program
{
    static void Main()
    {
        NotificationService service = new NotificationService();

        Notify notifyAll = service.Email;
        notifyAll += service.SMS;
        notifyAll += service.PushNotification;

        notifyAll(); // Invokes all three methods in order

        // Output:
        // Email sent.
        // SMS sent.
        // Push Notification sent.
    }
}

Behind the scenes, delegates maintain an invocation list, which is an ordered list of all the methods that need to be invoked.

Methods can be added to the delegate using the + or += operator and can be removed using the - or -= operator.

Multicast Delegates are especially useful when you want to notify multiple components or perform some chained tasks.

There is only one downside to it, when it comes to returning value from a multicast delegate, you must keep in mind that it only returns the value returned by the last method called.

The multicast delegates are good when used with side-effect-based methods, but for the methods that return a result, you need to stick with simple delegates or keep an eye if you are using multicasts.

It was quite overwhelming, right? And quite confusing too. But don’t worry. You will rarely need to write your own custom delegate.

As Microsoft has already provided some built-in delegates that cover up pretty much most of the use cases, and they are generic, so you can use them with any data type.

Built-In Delegates

Let’s see these built-in Delegates.

Action<T>

For methods that return void. You can have any number of parameters.

Action<string> print = msg => Console.WriteLine("Hello, " + msg);
print("World"); // Output: Hello, World

// with multiple parameters
Action<int, int> add = (a, b, c) => Console.WriteLine(a + b + c);
add(3, 4, 5); // Output: 12

Func<T>

For methods that return a value.

When the Func delegate is defined, then the last type inside the <> is the return type, and all the others are input types.

// The last type in the Func<> is the return type, all the others are input parameter types
Func<int, int, int> multiply = (a, b) => a * b;
int result = multiply(3, 5); // Output: 15

Predicate<T>

For methods that returns a bool.

This is just a shorthand for Func<T, bool>. And one of the most useful delegate.

💡 Did you know?

You’ve probably been using predicates already without knowing it — any lambda returning bool in LINQ is a predicate!

Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // Output: True

// Using predicates in LINQ
List<int> numbers = new List<int> { 2, 3, 6, 7, 8, 10, 11 };

// This lambda (n => n % 2 == 0) is a Predicate<int>
// and Where is a LINQ method
var evenNumbers = numbers.Where(n => n % 2 == 0);

foreach (var num in evenNumbers)
{
    Console.Write(num + " ");
}

// Output:
// 2 6 8 10

So, you might be wondering when we need to define custom delegates?

Well, the answer is, normally, you wouldn’t be defining custom delegates.

Unless you:

  • need a more descriptive name for readability or documentation.
  • want to define a specialised signature for consistency.
  • are building libraries or writing framework-level code, where named delegates improve clarity.

In summary, delegates give you the power to decouple what is done from how and when it’s done.

Think of them as the remote controls of your application logic — letting you pass, call, and combine behaviour dynamically while preserving type-safety.

They form the backbone of events, design patterns like Strategy and Observer, and features like LINQ, asynchronous programming, and responsive UIs. While custom delegates are great for learning the fundamentals, built-in delegates like Action, Func, and Predicate make them practical and powerful in everyday development.

Whether you’re writing reusable APIs, designing extensible systems, or building reactive UIs, understanding delegates can elevate your C# development from functional to elegant.

Mastering this elegant feature can take your code to the next level — cleaner, more flexible, and architecturally sound.

🎯 So don’t just understand delegates — start using them.

“Why Is My API Crying?” — Meet Messaging Queues, the Unsung Heroes of Scalability

About Author

Arpit Shukla

Solution Analyst

Hi, I'm Arpit. By day, I build full-stack systems in C# and .NET. By night, I'm chasing the parts of AI that genuinely excite me — RAG pipelines, agents, and the strange new craft of building software with models, not just around them. I write about what I learn, what trips me up, and what I'd tell my past self if I could. Pull up a chair. We're all figuring this out as we go.