Introduction
Picture this. You're working on an e-commerce project, and your Product Owner walks up to you and says, "Whenever an order is placed, I want us to send an email, send an SMS, log it for audit, and notify the warehouse. Oh, and next sprint we might add WhatsApp notifications too."
If you're like most developers, your first instinct is to write a method called PlaceOrder() and just call SendEmail(), SendSms(), WriteAuditLog(), and NotifyWarehouse() one after another inside it. It works. It ships. Everyone's happy.
Until three months later, when someone asks you to make notifications configurable per customer, or skip SMS for corporate accounts, or add a new notification channel without touching the order processing logic. Suddenly that neat little method turns into a nightmare of if-else blocks.
This is exactly the kind of problem delegates were built to solve. In this article, we'll start from zero — assuming you've never touched a delegate — and walk all the way up to how they power real ASP.NET Core applications, LINQ, middleware, and even the Task you use every day. By the end, you won't just know the syntax. You'll know why delegates exist and when reaching for one actually makes your code better instead of fancier.
Let's get into it.
What Problem Do Delegates Solve?
Here's a simple, real-life analogy before we touch any code.
Think about a food delivery app. When your order is out for delivery, you get notified — but not always the same way. Some people get a push notification. Some get an SMS. Some get both. The delivery app doesn't hardcode "always send push notification" inside its core delivery-tracking logic. Instead, it just says, "Hey, delivery status changed — whoever is listening, do your thing." The actual notification method is decided separately.
Now compare that to how we usually write code when we're in a hurry:
public void UpdateDeliveryStatus(Order order)
{
order.Status = "Out for Delivery";
SendPushNotification(order);
SendSms(order);
}
This looks fine on day one. But look closely — UpdateDeliveryStatus now knows about push notifications and SMS. It's tightly coupled to them. If tomorrow you need to add email, you edit this method. If you need to remove SMS for a specific plan, you edit this method again. Every small business change forces you to open and modify code that, honestly, has nothing to do with "updating delivery status."
This is tight coupling — one piece of code knowing too much about another piece of code it shouldn't have to care about.
What we actually want is a way to say: "When the delivery status changes, call whatever method is supposed to handle it — I don't want to know or care what that method is right now." That's a callback. And in C#, the way we represent "a method that will be called later, without hardcoding which one" is a delegate.
What is a Delegate?
Let's keep this simple. Forget the textbook definition for a second.
A delegate is just a variable — but instead of holding a number or a string, it holds a reference to a method. That's it. Just like an int variable holds a number, a delegate variable holds a method that you can call later.
Why did C# even introduce this? Because in the real world, we often don't know in advance which method should run. We only know the shape of the method — what parameters it takes and what it returns. Delegates let you say, "I'll accept any method that matches this shape, and I'll decide which actual method to plug in later."
Think of a delegate as a socket, and any matching method as a plug. As long as the plug fits the socket (same parameters, same return type), it works — regardless of which specific plug you use.
Delegate Syntax
Let's look at the actual syntax, piece by piece.
1. Declaring a Delegate
public delegate int MathOperation(int a, int b);
This line says: "I'm defining a type called MathOperation. Any method that takes two int parameters and returns an int can be assigned to it." Notice we're not writing any logic here — just describing the shape.
2. A Method That Matches the Shape
public static int Add(int a, int b)
{
return a + b;
}
Add takes two ints and returns an int — exactly matching MathOperation. That's what makes it eligible.
3. Creating a Delegate Instance
MathOperation operation = Add;
Here, operation is now pointing to the Add method. We haven't called Add yet — we've just stored a reference to it.
4. Invoking the Delegate
int result = operation(5, 3); // result = 8
Calling operation(5, 3) actually runs Add(5, 3) behind the scenes. This is the part that trips people up initially — you're not calling Add directly. You're calling it through the delegate.
Step-by-Step Simple Example: A Calculator
Let's build this out fully with a calculator, since it makes the "same socket, different plug" idea click instantly.
public delegate int Calculator(int a, int b);
public class MathOperations
{
public static int Add(int a, int b) => a + b;
public static int Subtract(int a, int b) => a - b;
public static int Multiply(int a, int b) => a * b;
}
Now let's use the same delegate variable to call three completely different methods:
Calculator calc = MathOperations.Add;
Console.WriteLine(calc(10, 5)); // 15
calc = MathOperations.Subtract;
Console.WriteLine(calc(10, 5)); // 5
calc = MathOperations.Multiply;
Console.WriteLine(calc(10, 5)); // 50
Look at what just happened. The variable calc didn't change type. We simply reassigned which method it points to. This is the core superpower of delegates — the calling code (calc(10, 5)) stays exactly the same, while the actual behavior underneath keeps changing.
Another Everyday Example: A Restaurant Order
Let's reinforce this with another analogy before we move to more advanced territory.
Imagine a restaurant kitchen. The waiter doesn't cook the dish himself — he just passes the order ticket to "whoever is responsible for that dish today." Maybe it's Chef A on Monday and Chef B on Tuesday. The waiter's job — take order, hand over ticket — never changes. Only the chef executing it changes.
public delegate string PrepareDish(string dishName);
public class KitchenStaff
{
public static string ChefA(string dish) => $"Chef A is preparing {dish}";
public static string ChefB(string dish) => $"Chef B is preparing {dish}";
}
PrepareDish todaysChef = KitchenStaff.ChefA;
Console.WriteLine(todaysChef("Pasta"));
todaysChef = KitchenStaff.ChefB;
Console.WriteLine(todaysChef("Pasta"));
The waiter (calling code) never needs an if-else to figure out which chef is cooking. That decision lives outside, and the delegate just points to whoever is currently responsible.
Multicast Delegates
So far, our delegate has pointed to just one method at a time. But delegates in C# can point to multiple methods at once — this is called a multicast delegate.
Why would you need this? Going back to our order example — when an order is placed, you might genuinely want multiple things to happen: send email, send SMS, write audit log. A multicast delegate lets one delegate call carry out all of them.
public delegate void OrderPlacedHandler(string orderId);
public class NotificationService
{
public static void SendEmail(string orderId)
=> Console.WriteLine($"Email sent for order {orderId}");
public static void SendSms(string orderId)
=> Console.WriteLine($"SMS sent for order {orderId}");
public static void WriteAuditLog(string orderId)
=> Console.WriteLine($"Audit log written for order {orderId}");
}
Now, instead of assigning one method, we chain multiple using +=:
OrderPlacedHandler handler = NotificationService.SendEmail;
handler += NotificationService.SendSms;
handler += NotificationService.WriteAuditLog;
handler("ORD1001");
Output:
Email sent for order ORD1001
SMS sent for order ORD1001
Audit log written for order ORD1001
All three methods ran, in the exact order they were added — this order is guaranteed. Internally, the delegate maintains something called an invocation list, which is just a list of all the methods attached to it.
You can remove a method the same way, using -=:
handler -= NotificationService.SendSms;
Now if you call handler("ORD1002"), SMS won't fire — only email and audit log will.
One thing worth knowing: if your multicast delegate has a return value (not void), only the last method's return value is kept — the earlier ones are silently discarded. This trips up a lot of developers, so stick to void or Action for multicast scenarios where you don't need each individual result.
Built-in Delegates: Action, Func, and Predicate
Writing a custom delegate every time gets repetitive fast. That's why .NET gives you three ready-made delegate types that cover almost every situation.
| Delegate | Returns | Use case |
|---|
| Action | void | When you want to do something, no result needed |
| Func | a value (last type parameter) | When you want to calculate or return something |
| Predicate | bool | When you want to check a condition |
// Action - no return value
Action<string> logMessage = message => Console.WriteLine(message);
logMessage("Order placed successfully");
// Func - returns a value (last type parameter is the return type)
Func<int, int, int> add = (a, b) => a + b;
int sum = add(4, 6); // 10
// Predicate - always returns bool
Predicate<int> isEven = number => number % 2 == 0;
bool result = isEven(8); // true
In everyday project work, you'll rarely need to declare your own delegate type anymore — Action and Func cover 95% of cases. Custom delegates are mostly useful when you want a meaningful, self-documenting name (like OrderPlacedHandler instead of Action), or when you're building library-level APIs.
Anonymous Methods
Before lambda expressions existed, C# gave us anonymous methods — a way to write the method body directly at the point of use, without giving it a separate name.
OrderPlacedHandler handler = delegate (string orderId)
{
Console.WriteLine($"Processing order {orderId}");
};
handler("ORD2001");
Why did this matter? Earlier, if you wanted a delegate to do something small and one-off, you were forced to write a full separate named method just for that. Anonymous methods let you skip that ceremony and write the logic right where you need it — useful for short, throwaway logic that doesn't deserve its own method name cluttering your class.
Lambda Expressions
Anonymous methods were a good start, but they were still a bit wordy. Lambda expressions, introduced in C# 3.0, made this dramatically shorter and became the standard way developers write inline delegate logic today.
OrderPlacedHandler handler = orderId =>
Console.WriteLine($"Processing order {orderId}");
handler("ORD3001");
Compare this to the anonymous method version above — same behavior, far less noise. This is why, in modern C# code, you'll almost always see lambdas instead of the delegate keyword.
The relationship is simple: a lambda expression is just a shorthand way of creating a delegate instance (or an Action/Func) without writing a separate named method. Under the hood, the compiler still generates a method and wires up a delegate pointing to it — you're just not seeing that machinery.
Real ASP.NET Core Project Example: Order Processing System
Let's now put everything together in something closer to a real enterprise scenario — an order processing pipeline.
public class OrderProcessor
{
public event Action<string> OrderPlaced;
public void PlaceOrder(string orderId)
{
// Core business logic - save to database
Console.WriteLine($"Order {orderId} saved to database");
// Notify whoever is listening - core logic doesn't know or care who
OrderPlaced?.Invoke(orderId);
}
}
Now, in a different part of the application — maybe a completely different service or module — we subscribe to this:
var processor = new OrderProcessor();
processor.OrderPlaced += orderId =>
Console.WriteLine($"Email service: sending confirmation for {orderId}");
processor.OrderPlaced += orderId =>
Console.WriteLine($"SMS service: sending alert for {orderId}");
processor.OrderPlaced += orderId =>
Console.WriteLine($"Audit service: logging order {orderId}");
processor.OrderPlaced += orderId =>
Console.WriteLine($"Warehouse service: notifying dispatch for {orderId}");
processor.PlaceOrder("ORD5001");
Notice something important: OrderProcessor has absolutely no idea that email, SMS, audit logging, or warehouse notification even exist. It just raises an event saying "an order was placed." Each service subscribes independently.
This means if tomorrow you need to add a WhatsApp notification service, you don't touch OrderProcessor at all — you just subscribe a new handler from wherever that service lives. This is the real payoff of delegates: your core business logic stops changing every time a peripheral requirement changes.
Here's a quick diagram of how this flows:
graph LR
A[PlaceOrder called] --> B[Order saved to DB]
B --> C[OrderPlaced event raised]
C --> D[Email Service]
C --> E[SMS Service]
C --> F[Audit Service]
C --> G[Warehouse Service]
One delegate call, four independent handlers, zero coupling between them.
Another Real Project Example: Payment Gateway Integration
Delegates also shine in scenarios involving multiple payment providers — a very common enterprise requirement.
public delegate PaymentResult ProcessPayment(decimal amount);
public class PaymentGateways
{
public static PaymentResult PayWithRazorpay(decimal amount)
{
Console.WriteLine($"Processing ₹{amount} via Razorpay");
return new PaymentResult { Success = true };
}
public static PaymentResult PayWithStripe(decimal amount)
{
Console.WriteLine($"Processing ${amount} via Stripe");
return new PaymentResult { Success = true };
}
}
public class PaymentResult
{
public bool Success { get; set; }
}
ProcessPayment paymentMethod = customerRegion == "India"
? PaymentGateways.PayWithRazorpay
: PaymentGateways.PayWithStripe;
PaymentResult result = paymentMethod(1500);
The checkout flow that calls paymentMethod(1500) doesn't need to know or care which gateway is actually behind it. Adding a third gateway later means adding one more method — the calling code doesn't change.
Delegates Behind the Scenes
Once you start noticing delegates, you'll realize they're everywhere in .NET — most developers use them daily without realizing it.
LINQ: Every .Where(x => x.Age > 18) or .Select(x => x.Name) is passing a delegate (specifically a Func) into the LINQ method.
Task.Run: Task.Run(() => DoWork()) accepts a delegate (an Action or Func) representing the work to execute on a background thread.
Thread: new Thread(() => DoWork()) — same idea, a delegate pointing to the method that thread should run.
Events: Every event keyword in C# is built directly on top of delegates. OrderPlaced in our example above is a delegate under the hood.
ASP.NET Core Middleware: The entire middleware pipeline is built using delegates. Each app.Use(...) call registers a delegate (RequestDelegate) that takes an HttpContext and decides whether to pass control to the next middleware.
Entity Framework: Methods like .Include(x => x.Orders) or .FirstOrDefault(x => x.Id == id) all rely on delegates (via expression trees, which are closely related) to describe what to fetch or filter.
Once this clicks, you'll start seeing Func, Action, and lambda expressions as the connective tissue running through almost every modern .NET feature.
Performance Considerations
Delegates are powerful, but they're not free. A few practical things worth knowing before you sprinkle them everywhere:
Allocation: Every time you create a delegate instance, .NET allocates a small object on the heap. In a tight loop creating thousands of delegates per second, this can add measurable GC pressure.
Invocation overhead: Calling a method through a delegate is slightly slower than calling it directly, because of an extra layer of indirection. For 99% of business applications, this difference is invisible — but it matters in hot paths like tight loops processing millions of records.
Lambda closures: A lambda that captures a variable from its surrounding scope (like orderId in our examples) creates a hidden class behind the scenes to hold that captured state. This is usually fine, but capturing large objects unnecessarily can hold onto memory longer than expected.
Multicast delegate cost: Each subscriber in the invocation list adds a bit of overhead. If one subscriber throws an exception, it stops the rest of the invocation list from running — something to be careful about in event-heavy systems.
None of this means "avoid delegates." It means: use them where they genuinely simplify your design, and be a little more careful in performance-critical, high-throughput code paths.
Common Mistakes
Signature mismatch: Trying to assign a method with a different parameter list or return type to a delegate — the compiler will catch this, but it confuses people new to delegates.
Null delegate invocation: Calling a delegate that has no subscribers throws a NullReferenceException. Always check handler?.Invoke(...) or use the null-conditional operator.
Too many multicast subscribers: Attaching dozens of handlers to a single event without tracking them can make debugging painful — you lose track of what actually runs.
Memory leaks via event subscriptions: If an object subscribes to another object's event but never unsubscribes, the publisher keeps a reference alive, preventing garbage collection. This is one of the most common causes of memory leaks in long-running .NET applications.
Overusing delegates where a simple method call would do: Not every method needs to be "pluggable." If there's genuinely only one implementation and no foreseeable need for flexibility, a direct method call is simpler and easier to read.
Best Practices
Prefer built-in Action/Func/Predicate over custom delegate types unless a custom name genuinely improves readability.
Give delegate variables and custom delegate types meaningful names — OrderPlacedHandler communicates intent far better than Action<string> handler1.
Keep the methods you assign to delegates small and focused — a delegate calling a 200-line method defeats the purpose of clean design.
Don't introduce a delegate "just in case you need flexibility later." Add it when the flexibility is actually needed.
Use event (not a plain public delegate field) when you're building a notification mechanism — it prevents external code from overwriting or clearing your invocation list.
Always unsubscribe from events when the subscriber's lifetime ends, especially in long-lived publishers.
Keep SOLID principles in mind — delegates are a natural fit for the Open/Closed Principle, since they let you extend behavior without modifying existing code.
Interview Questions
1. What is a delegate in C#?
A type-safe object that holds a reference to a method with a matching signature, allowing that method to be called indirectly through the delegate.
2. How is a delegate different from a normal method call?
A normal call is fixed at compile time to one specific method. A delegate call can point to different methods at runtime, decided by whatever the delegate is currently assigned to.
3. What is a multicast delegate?
A delegate that holds references to more than one method, all of which get called in order when the delegate is invoked.
4. What happens if a multicast delegate with a return type is invoked?
Only the return value of the last method in the invocation list is returned; the rest are discarded.
5. What's the difference between Action, Func, and Predicate?
Action doesn't return a value, Func returns a value (defined by its last type parameter), and Predicate always returns a bool, typically used for conditions.
6. Are events and delegates the same thing?
Events are built on top of delegates but add restrictions — outside classes can only subscribe (+=) or unsubscribe (-=), not directly invoke or overwrite the delegate.
7. What is the difference between an anonymous method and a lambda expression?
Both let you write inline method logic without a separate named method. Lambdas are simply a more concise syntax and are the modern standard; anonymous methods use the delegate keyword and are rarely used in new code.
8. Can a delegate cause a memory leak?
Yes — if a subscriber never unsubscribes from a publisher's event, the publisher holds a reference to the subscriber, preventing it from being garbage collected.
9. Where does ASP.NET Core use delegates internally?
Middleware components are registered as delegates (RequestDelegate), each deciding whether to handle a request or pass it to the next middleware in the pipeline.
10. Why would you use a custom delegate instead of Func or Action?
When a custom name makes the code significantly more readable, or when you're designing a public API/library where a well-named delegate type communicates intent better than a generic Func/Action.
Summary
Delegates start out looking like a small syntax detail, but once you see them clearly, they turn out to be one of the most quietly powerful ideas in C#. They let your code say "call whatever handles this" instead of "call this exact method" — and that one shift is what makes your systems flexible enough to grow without constant rewrites.
From a simple calculator example, to multicast notifications, to the entire ASP.NET Core middleware pipeline — it's the same core idea repeating at every scale: a variable that points to a method, decided at runtime instead of hardcoded at compile time.
The best way to really internalize this is to go back to your own codebase and look for if-else chains deciding "which method to call based on some condition." There's a good chance a Func, Action, or a well-placed event could make that code simpler, more testable, and easier to extend. Try refactoring one of those spots this week — that's where delegates stop being theory and start becoming a tool you reach for naturally.