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
delegatekeyword defines that this is a delegate type. boolorvoidis your return type.- The
SendConfirmationDelegatename 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
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

