In the last post we covered generics, which was a class with a blank for a type. Delegates are a different kind of idea: a variable that holds a method. If that sentence feels strange, that is normal. It felt strange to me too, and then it clicked, and half of modern C# suddenly made sense. We build it one small step at a time.
A variable that holds a method
You already know what a variable does. It holds data:
int price = 20;
string title = "The Great Gatsby";
Here is the new idea. A variable can also hold a method. Not the result of calling the method, the method itself.
The whole trick sits in one tiny detail, the brackets:
SayHello // the method itself, the recipe, can be passed around or run later
SayHello() // run it now
Think of the method name as a power button. SayHello is the button itself, something you can hand to someone else to press whenever they choose. SayHello() is pressing it right now.
A delegate is the thing that stores the button.
Declaring the shape
A delegate cannot hold just any method. It holds methods of a specific shape, meaning a specific return type and specific parameters. So before you can store a method, you declare what shape of method the delegate accepts:
public delegate void Greeting(string name);
Reading it piece by piece:
delegatemeans we are defining a delegate typevoidmeans the methods it holds return nothingGreetingis the name of this delegate type(string name)means the methods it holds take one string
So Greeting can hold any method that returns void and takes one string. Nothing else fits, and the compiler enforces that.
Storing and calling through a delegate
Here is the full runnable example. Two methods that match the Greeting shape:
// Book.cs
public delegate void Greeting(string name);
public class Book
{
public string Title { get; set; }
public Book(string title)
{
Title = title;
}
// Both methods match the Greeting shape: return void, take one string.
public void SayHello(string name)
{
Console.WriteLine($"Hello, {name}!");
}
public void SayGoodbye(string name)
{
Console.WriteLine($"Goodbye, {name}!");
}
}
Now store one in the delegate, call it, then swap what it holds and call the identical line again:
// Program.cs (top-level statements)
var book = new Book("The Great Gatsby");
Greeting greetingDelegate = book.SayHello; // store the method, no brackets
greetingDelegate("Alice"); // Hello, Alice!
greetingDelegate = book.SayGoodbye; // swap what it holds
greetingDelegate("Alice"); // Goodbye, Alice!
Look closely at what just happened. The call line greetingDelegate("Alice") never changes, but the output does, because the delegate points at a different method each time. The caller is decoupled from the specific method it runs. That sentence is the entire point of delegates, and the rest of this post is just that sentence at bigger and bigger scales.
The one word that matters, indirection
The value of a delegate is indirection. Your calling code says “run whatever is in this slot” without knowing what is in the slot. Someone else fills the slot.
Think of a power socket. The socket does not care whether you plug in a lamp or a laptop. It provides the electricity and runs whatever is plugged in. The delegate is the socket, the method is the appliance.
That indirection enables three things plain method calls cannot do:
- Pass behaviour as an argument. A method can take another method as a parameter.
- Store behaviour for later. Hold a method now, run it when an event happens.
- Swap behaviour at runtime without changing the calling code, exactly like
greetingDelegateabove.
Action and Func, you rarely declare your own delegate
Declaring a delegate type for every shape gets old fast, so modern C# ships two generic delegates that cover almost everything:
Action<string> // takes a string, returns void
Func<int, bool> // takes an int, returns a bool (last type is always the return type)
Action is for methods that return nothing, Func is for methods that return a value, and the last generic type in a Func is always the return type. Our custom Greeting delegate is just Action<string> with a name. In day to day code you will mostly use these two and almost never write the delegate keyword yourself. But everything they do is exactly what you just learned.
Where delegates run in production
This is not an academic feature. Delegates are load bearing in every serious .NET codebase, usually without you noticing.
LINQ is delegates end to end. When you write this:
var expensive = books.Where(b => b.Price > 20);
you are passing a Func<Book, bool> into Where. The Where method does not know your condition. It was written years before your Book class existed. You hand it the deciding behaviour, it runs your logic against every item. One method, any condition, because the condition is a delegate.
Event handling and UI callbacks. In UI frameworks this line stores your method in the button’s slot:
button.Clicked += OnCheckoutPressed;
The button runs whatever is in the slot when it is clicked. The button class was written before your checkout logic existed, and it never needs to know what your handler does.
The ASP.NET Core middleware pipeline. Every request on the sites I build flows through a chain of middleware, and each step is a delegate. next points at the following step:
app.Use(async (context, next) =>
{
Console.WriteLine($"Request: {context.Request.Path}");
await next(); // run whatever step comes after me
});
The framework calls the chain in order without knowing what you added to it.
Retry, caching, and wrap-an-operation patterns. A method can take your code as a delegate and add behaviour around it:
public T WithRetry<T>(Func<T> operation)
{
for (var attempt = 1; ; attempt++)
{
try { return operation(); }
catch when (attempt < 3) { } // swallow twice, then let it throw
}
}
var books = WithRetry(() => LoadBooksFromApi());
WithRetry runs your operation and retries it on failure, without knowing what the operation is. Resilience libraries like Polly are built on exactly this shape.
Async callbacks and background jobs. “Do this work, and when it finishes, run this method.” The completion handler is a delegate stored until the work is done. async and await are a cleaner syntax layered over this idea.
The through-line in all five: framework authors write code that must run your logic, but they wrote it before your logic existed. So they leave a slot, a delegate, and you fill it. Delegates are how reusable frameworks and specific business logic meet.
The same idea inside Unity, Meta and Stripe
Like generics, this concept is not locked to C#. Storing a function in a slot and running it later is one of the most used ideas in software, and you can point at it inside the biggest products in the world.
Unity (C#). Unity is the most direct example because it literally is C# delegates. Millions of games wire their UI like this:
buyButton.onClick.AddListener(BuyItem); // store the method, no brackets
onClick is a slot on a button that Unity’s engineers wrote years ago. Your BuyItem method fills it, and Unity presses the power button when the player clicks. Every tutorial you have ever seen with AddListener is the exact Greeting example from this post.
Meta (React, JavaScript). In React the no-brackets rule appears on almost every line of UI code:
<button onClick={handleCheckout}>Buy</button>
handleCheckout is passed without brackets, because you are handing React the function itself, not the result of calling it. React stores it and runs it on click. JavaScript treats functions as values everywhere, which is delegates as a language default, and the entire event system at facebook.com sits on it.
Netflix (Node.js). Netflix runs its API layer on Node.js, and Node is a callback engine at its core. A server is one big slot:
server.on('request', handleRequest);
The runtime stores your handler and calls it for every incoming request. Node’s whole event loop is the “store behaviour for later” bullet from earlier, at the scale of hundreds of millions of users.
Stripe and GitHub (webhooks). A webhook is a delegate stretched over the network. You register a URL with Stripe, and when a payment succeeds Stripe calls your URL with the event data. Stripe’s engineers wrote that code long before your endpoint existed. They left a slot, you filled it with your handler. Same idea, except the method lives on a different server.
The takeaway matches the generics post: AddListener, onClick={fn}, server.on, webhooks. Different companies, different languages, one concept. A slot that holds behaviour, filled by code that came later.
Summary
- A delegate is a type-safe pointer to a method.
- Method name without brackets is the method itself, with brackets runs it now.
- You declare the shape (return type and parameters), then store any matching method.
- The value is indirection: the caller runs a slot without knowing what fills it.
ActionandFuncare the built-in generic delegates that cover most cases.- LINQ, events, middleware, retry logic, and async callbacks are all delegates underneath.
Interview questions and answers
1. What is a delegate in C#?
A type-safe pointer to a method. It is a variable that holds a method with a specific shape (return type and parameters), so it can be stored, passed around, and called later.
2. What is the difference between SayHello and SayHello()?
Without brackets is the method itself, which can be stored in a delegate or passed as an argument. With brackets runs the method now.
3. Why declare the delegate’s return type and parameters?
Because a delegate is type-safe. It can only hold methods matching that exact shape, and the compiler enforces it, so you cannot store a method that does not fit.
4. What problem do delegates solve?
Indirection. They let calling code run behaviour without knowing what that behaviour is, so frameworks can run your logic even though they were written before it existed. This enables passing behaviour as an argument, storing it for later, and swapping it at runtime.
5. What are Action and Func?
Built-in generic delegates. Action holds a method that returns void, Func holds a method that returns a value where the last generic type is the return type. They remove the need to declare custom delegate types in most cases.
6. How do delegates relate to LINQ?
LINQ methods take delegates. Where takes a Func<T, bool>, so it can filter by any condition you supply. The method is written once and works for any predicate because you pass the deciding logic in.
7. How do delegates power events?
An event exposes a slot that holds methods. You add a handler with +=, and when the event fires the stored methods run. The publisher does not know what the handlers do.
8. Can a delegate hold more than one method?
Yes, this is a multicast delegate. Using += adds methods, and calling the delegate runs them all in order. This is how one event can notify multiple handlers.
9. What is the difference between a delegate and an interface?
Both let you pass behaviour, but a delegate points at a single method whereas an interface groups several related methods. Delegates suit single-method callbacks, interfaces suit a set of related operations.
10. Are delegates still needed given lambdas exist?
Yes. A lambda is just a compact way to write the method that a delegate holds. The lambda still needs a delegate type (usually Action or Func) to be stored in. Lambdas are the syntax, delegates are the type.