Skip to main content

Understanding Delegates in C#

In this section you will learn what delegates are all about, why they are so super useful, and how you can use them in an effective way in your own applications.

Ever heard of delegates? If you ask me I would say it is one of the most powerful features in C# and really opened my eyes for the language when I first found out about them.

Yeah... they can be confusing for anyone new to C# I know that, but stay calm and follow along. I promise you will be eager to implement them after these three short posts.

What Are Delegates?

So.. what is it all about? A simple explanation would be to say that a delegate is a type-safe function pointer or a reference type and all it is doing is holding a reference to a method.

You can think of a delegate as a contract that dictates/defines/says how or what a method should look like (a kind of a signature), and then you can pass around references to methods that match that contract. Smart right? πŸ”₯

In other words - a delegate makes it possible to "treat" your methods as variables that can be passed as a parameter, kept in a collection or invoked whenever you need it.

Why Use Delegates?

Delegates will give you several great things like:

  • Callback mechanisms. These will make it possible to execute code at specific points without tight coupling. What's not to like!? πŸ‘
  • Event handling. This is the foundation for the event-driven programming model in .NET/C#.
  • Flexibility. You can pass different behaviors to methods at runtime like i mentioned above.
  • Decoupling. This will make it possible to separate the caller from the implementation details.

How to Declare a Custom Delegate?

Okay, I promised to give you some simple and practical examples. Let's start with the bare minimum or should we say basics. This is how you declare a custom delegate in C#.

public delegate bool SendConfirmationDelegate(string message);
public delegate void TaskCompletedDelegate();

What is going on? πŸ€”

  • The delegate keyword defines that this is a delegate type.
  • bool or void is your return type.
  • The SendConfirmationDelegate name is the name of the delegate.
  • A as we know it from methods, the parameters are defined in parentheses.

So... above are two new delegates. They create new types that can reference any method that has a matching signature

These declarations create new types that can reference any method with a matching signature. Remember the signature I mentioned earlier?

Real-World Example - A Notification System

I learn by seeing some real examples, and not just a snip of some code, and then the person behind the documentation expects that I totally get it. If you are like me, you have come to the right place.

Let's create a real-world example. I will now show you how to build a notification system in .NET.

Declaring The Delegates

public class NotificationService
{
    // Declare delegate types
    public delegate bool SendMessageDelegate(string message);
    public delegate void TaskCompletedDelegate();

    public void ProcessNotification(
        string message,
        SendMessageDelegate sendMessage,
        TaskCompletedDelegate onComplete = null)
    {
        Console.WriteLine("Processing notification...");
        
        // Invoke the send message delegate
        bool success = sendMessage?.Invoke(message) ?? false;
        
        if (success)
        {
            Console.WriteLine("βœ“ Message sent successfully");
        }
        else
        {
            Console.WriteLine("βœ— Failed to send message");
        }
        
        // Invoke completion callback
        onComplete?.Invoke();
    }
}

That code look pretty simple. Did you notice the null-conditional operator ?.?That makes it possible for us to safely invoke the delegate only if it's not null.

Using the Notification Service

Okay, we have the implementation in place for the notification system. Let's now take a look at how to use it from different methods.

var notificationService = new NotificationService();

// Send via Email
notificationService.ProcessNotification(
    message: "Your order has shipped!",
    sendMessage: SendEmail,
    onComplete: LogCompletion);

bool SendEmail(string message)
{
    Console.WriteLine($"πŸ“§ Email sent: {message}");
    return true;
}

void LogCompletion()
{
    Console.WriteLine("βœ“ Notification logged");
}

// Send via SMS using lambda expressions
notificationService.ProcessNotification(
    message: "Your package arrives tomorrow",
    sendMessage: (msg) => 
    {
        Console.WriteLine($"πŸ“± SMS sent: {msg}");
        return true;
    },
    onComplete: () => Console.WriteLine("βœ“ SMS notification complete"));

Did you just see a bright light? πŸ’‘ πŸ˜…

The two examples above shows how much power the delegates gives us. The NotificationService is not aware or actually doesn't evene care about how the messages are sent.

This gives you the option to plug-and-play new options for notifications, like:

  • SMS
  • Emails
  • Push Notifications
  • etc...

Multicast Delegates - Notify Multiple Recipients

If you got amazed or happy about the examples above due to the power you are given, you are going to be super happy now! πŸ˜„ Delegates can hold references to multiple methods πŸ”₯ This is referred to as a multicast delegate.

Creating a logger with multiple destinations

Let's take a practical example that I often practice in API's and systems in general. Logging to multiple destinations, but keeping it simple and maintainable for everyone.

public class Logger
{
    // Delegate that takes a message
    public delegate void LogMessageDelegate(string message);
    
    public void LogImportantEvent(string message)
    {
        // Create a multicast delegate
        LogMessageDelegate logger = WriteToConsole;
        logger += WriteToFile;
        logger += SendToMonitoring;
        
        // Single invocation calls all three methods
        Console.WriteLine("Logging event to all destinations...");
        logger(message);
    }
    
    private void WriteToConsole(string message)
    {
        Console.WriteLine($"[Console] {message}");
    }
    
    private void WriteToFile(string message)
    {
        Console.WriteLine($"[File] {message}");
    }
    
    private void SendToMonitoring(string message)
    {
        Console.WriteLine($"[Monitoring] {message}");
    }
}

// Usage
var logger = new Logger();
logger.LogImportantEvent("System started successfully");

/* Output:
Logging event to all destinations...
[Console] System started successfully
[File] System started successfully
[Monitoring] System started successfully
*/

When you call/invoke a multicast delegate, all the methods you have declared in the delegate are called in the order they have been defined. Awesome right?! πŸ™Œ

A few things I would mention, that I think are crucial or important to know are:

  • If you delegate has a return type, you will only get the value from the last method that was invoked in the delegate.
  • If any of the methods in your delegate throws an exception, any upcoming/next methods won't be executed/invoked.
  • You can use the += operator to add methods or -= to remove them again.

Lambda Expressions with Delegates

Lambda expressions is something most devs knows. Especially if you have been working with LINQ πŸ˜„ Using lambdas will make your code for the delegates more concise.

Let's take a look at a practical example where we will create a very simple payment processor.

public class PaymentProcessor
{
    public delegate void PaymentDelegate(decimal amount);
    
    public void ProcessPayment(decimal amount, PaymentDelegate processMethod)
    {
        Console.WriteLine($"Processing ${amount}...");
        processMethod(amount);
    }
}

// Usage with different payment methods
var processor = new PaymentProcessor();

// Traditional method reference
processor.ProcessPayment(100.00m, ChargeCreditCard);

void ChargeCreditCard(decimal amount)
{
    Console.WriteLine($"πŸ’³ Charged ${amount} to credit card");
}

// Lambda expression - inline
processor.ProcessPayment(50.00m, (amount) =>
{
    Console.WriteLine($"πŸ’° Processed ${amount} via PayPal");
});

// Lambda expression - single line
processor.ProcessPayment(25.00m, amt => Console.WriteLine($"πŸ“± Processed ${amt} via Apple Pay"));

As you can see, lambda expressions are very useful if you have some simple, one-off logic that you don't need to reuse.

Practical Example - Data Processing Pipeline

Let's take another round looking at a practical example. In this example I will show you how to create a flexible data processing setup using delegates.

public class DataProcessor
{
    public delegate bool ValidationDelegate(string data);
    public delegate string TransformDelegate(string data);
    
    public void ProcessData(
        string input,
        ValidationDelegate validator,
        TransformDelegate transformer)
    {
        Console.WriteLine($"Input: {input}");
        
        // Validate
        if (!validator(input))
        {
            Console.WriteLine("❌ Validation failed");
            return;
        }
        
        // Transform
        string result = transformer(input);
        Console.WriteLine($"Output: {result}");
    }
}

// Usage
var processor = new DataProcessor();

processor.ProcessData(
    input: "hello world",
    validator: (data) => !string.IsNullOrEmpty(data),
    transformer: (data) => data.ToUpper());

processor.ProcessData(
    input: "test@christian-schou.com",
    validator: (email) => email.Contains("@"),
    transformer: (email) => $"User: {email.Split('@')[0]}");

/* Output:
Input: hello world
Output: HELLO WORLD

Input: test@christian-schou.com
Output: User: test
*/

Two Ways to Invoke Delegates

Delegates can be invokes in two ways in C#, this is how you can do it.

public delegate void MessageDelegate(string msg);

MessageDelegate handler = (msg) => Console.WriteLine(msg);

// Method 1: Direct invocation
handler("Hello");

// Method 2: Using Invoke method (safer with null checking)
handler?.Invoke("World");

Are there any difference? Nope 😎 Both ways gives you the same result. I prefer option two, where you explicitly use the Invoke() method as it is more descriptive and works great in condition with null-conditional operators.

Building a Simple Task Scheduler

Okay, let's take another practical example that a lot of applications often has some sort of, a task scheduler. This one will be my last practical example in this post, and will tie everything together that I have explained and showed above.

public class TaskScheduler
{
    public delegate void TaskDelegate(string taskName);
    
    public void RunTasks(params (string name, TaskDelegate task)[] tasks)
    {
        Console.WriteLine($"Running {tasks.Length} tasks...\n");
        
        foreach (var (name, task) in tasks)
        {
            Console.WriteLine($"Executing: {name}");
            task?.Invoke(name);
            Console.WriteLine();
        }
        
        Console.WriteLine("All tasks completed!");
    }
}

// Usage
var scheduler = new TaskScheduler();

scheduler.RunTasks(
    ("Backup Database", (name) => Console.WriteLine($"  βœ“ {name} completed")),
    ("Send Reports", (name) => Console.WriteLine($"  βœ“ {name} sent to admin")),
    ("Clear Cache", (name) => Console.WriteLine($"  βœ“ {name} cleared successfully"))
);

/* Output:
Running 3 tasks...

Executing: Backup Database
  βœ“ Backup Database completed

Executing: Send Reports
  βœ“ Send Reports sent to admin

Executing: Clear Cache
  βœ“ Clear Cache cleared successfully

All tasks completed!
*/

When building applications, it is very common practice to run small tasks in sequence. This could be stuff like making a backup, sending a data report, etc... In my example above, we are using the built-in task scheduler in .NET and it accepts any number of taks, that each has a name a small amount of code to execute.

Building you applications like I have shown above will make your code easy to read, easy to extend and let it stay organized. This way it won't be a nightmare to introduce another step in your sequence of tasks that goes into making a backup.

The final result using delegates, and not just in the example above, but in any application is a clean and super flexible way to chain operations.

Key Takeaways

As I mentioned at the very beginning, delegates in C# is a type-safe way of referencing methods. They will make your applications flexible as they allow you to pass behavior as parameters - that is pretty cool right?! πŸ”₯ If you ask me, it's the perfect choice if you want to make your code clean and modular at the same time without reinventing the wheel. One very nice feature I really like are the multicast delegates. They allows us to hold multiple methods and trigger all of them in one go.

If you are looking for a quick and readable delegate, stick to the lambda expressions. They will make your code concise and easy to maintain. I have been asked quite a few times about the usage of the null-conditional operator, and I always tell developers that it is good practice to use it, as it will prevent null reference exceptions when invoking the delegates.

What's Next?

This post was about delegates. Now I think it is time for you to take a look at events. Events build upon delegates (see what I did there), and they will give us even more power in C# to create e.g. a publish-subscribe pattern.

Resources

Work with delegate types in C# - C#
Explore delegate types in C#. A delegate is a date type that refers to a method with a defined parameter list and return type. You use delegates to pass methods as arguments to other methods.
Delegates - C# language specification
This chapter defines delegates, which are objects that hold type safe function pointers.
Updated on Nov 15, 2025