Say you run checkout for a Denver outdoor store, and marketing changes the discount every week: 10% off one week, $15 off orders over $100 the next.
Delegates in C# handle that cleanly. A delegate is a type-safe variable that holds a method, so checkout calls whichever rule you plug in at run time, with no if chain to edit.
Quick answer: declare delegate decimal DiscountRule(decimal s);, assign a method or lambda, and call it like a method. Prefer Func and Action when you don’t need a named type.
What Is a Delegate in C#?
A delegate type describes a method signature: a return type and a parameter list. Any static or instance method with a compatible signature can be stored in a variable of that type and called through it later.
Think of it as a variable whose value is one of your C# methods instead of a number or a string. For delegates, the return type is part of the signature, unlike with method overloading.
Here’s the store’s DiscountRule type, filled three ways on an $80 cart (and $120 for the lambda):
using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");
// 1. Method group: the compiler creates the delegate
DiscountRule rule = TenPercentOff;
Console.WriteLine($"Method group: {rule(80m):C}");
// 2. Explicit new (C# 1 syntax, still valid)
rule = new DiscountRule(FlatFiveOff);
Console.WriteLine($"new + method: {rule(80m):C}");
// 3. Lambda expression
rule = s => s >= 100m ? s - 15m : s;
Console.WriteLine($"Lambda: {rule.Invoke(120m):C}");
static decimal TenPercentOff(decimal s) => s * 0.90m;
static decimal FlatFiveOff(decimal s) => s - 5m;
delegate decimal DiscountRule(decimal subtotal);
Output:
Method group: $72.00
new + method: $75.00
Lambda: $105.00

TenPercentOff returned $72.00, FlatFiveOff returned $75.00, and the lambda took $15 off $120 for $105.00. You can call rule(80m) or rule.Invoke(80m); they do exactly the same thing.
| Way to create it | Syntax | When I use it |
|---|---|---|
| Method group | rule = TenPercentOff; | The method already exists and has a name |
Explicit new | new DiscountRule(FlatFiveOff) | Only in old code; the method group does the same |
| Lambda | rule = s => s - 15m; | Short, one-off logic written inline |
Note: in a file-based app, the delegate declaration goes after the top-level statements, like any other type. Every delegate type derives from System.MulticastDelegate and is sealed.
Pass a Delegate as a Callback: Swap Discount Rules at Run Time
The real payoff is passing a delegate into a method. Checkout doesn’t know which discount it’s applying; it just calls the DiscountRule it was handed.
Same store, now with three customers and a dictionary that maps promo codes to rules. Carmen has no promo code, so she gets a do-nothing none rule:
using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");
var rules = new Dictionary<string, DiscountRule>
{
["SAVE10"] = s => s * 0.90m,
["SHIP15"] = s => s >= 100m ? s - 15m : s,
};
DiscountRule none = s => s;
Checkout("Jenna Morales", 89.50m, rules["SAVE10"]);
Checkout("Tyler Brooks", 142.00m, rules["SHIP15"]);
Checkout("Carmen Diaz", 64.25m, none);
static void Checkout(string who, decimal sub,
DiscountRule rule) =>
Console.WriteLine($"{who}: {sub:C} -> {rule(sub):C}");
delegate decimal DiscountRule(decimal subtotal);
Output:
Jenna Morales: $89.50 -> $80.55
Tyler Brooks: $142.00 -> $127.00
Carmen Diaz: $64.25 -> $64.25

Jenna’s $89.50 cart dropped to $80.55 with SAVE10, Tyler’s $142.00 became $127.00 with SHIP15, and Carmen’s none rule left her at $64.25. The same Checkout method produced all three.
When marketing sends next week’s promo, you add one dictionary line. That’s the pattern Microsoft Learn calls late binding: the caller supplies part of the algorithm.
Func, Action and Predicate: The Built-in Delegates
You rarely need your own delegate type. .NET ships generic ones that cover almost every signature:
| Delegate | Returns | Example | Typical use |
|---|---|---|---|
Func | The last type argument | Func<decimal, int, decimal> | Calculations, LINQ selectors |
Action | Nothing (void) | Action<string> | Logging, notifications |
Predicate | bool | Predicate<decimal> | List.FindAll, Exists, RemoveAll |
Func and Action come in versions that take up to 16 parameters. Same checkout again: four $24.99 items, Austin’s 8.25% tax, and a check for big orders:
using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");
Func<decimal, int, decimal> lineTotal = (p, q) => p * q;
Func<decimal, decimal> tax = s => Math.Round(s * 1.0825m, 2);
Action<string> log = m => Console.WriteLine($"[log] {m}");
Predicate<decimal> isBig = t => t >= 100m;
decimal total = tax(lineTotal(24.99m, 4));
log($"Order total {total:C}");
List<decimal> totals = [89.50m, 142.00m, 64.25m, 210.75m];
List<decimal> big = totals.FindAll(isBig);
log($"Big orders: {string.Join(", ", big)}");
log($"LINQ count: {totals.Count(t => t >= 100m)}");
Output:
[log] Order total $108.21
[log] Big orders: 142.00, 210.75
[log] LINQ count: 2

The order came to $108.21, and FindAll on a List in C# returned the two totals of $100 or more. LINQ’s Count takes a Func<decimal, bool> instead, and it also counted 2.
Important: Predicate<T> and Func<T, bool> have the same shape but are different types, so you can’t pass a Predicate variable to Where. Pass a lambda, or isBig.Invoke.
I still write a custom delegate when the name documents intent (DiscountRule reads better than Func<decimal, decimal>), or when I need ref or out parameters.
Multicast Delegates: +=, -= and the Return Value
One delegate can hold several methods in its invocation list. += adds a method, -= removes it, and calling the delegate runs them in the order they were added.
When order 1047 is placed, the store emails the customer, texts them and tells the warehouse:
Action<string>? notify = SendEmail;
notify += SendText;
notify += id => Console.WriteLine($"Warehouse: pick {id}");
notify("order 1047");
int handlers = notify.GetInvocationList().Length;
Console.WriteLine($"Handlers: {handlers}");
notify -= SendText; // customer turned off texts
notify?.Invoke("order 1048");
Func<decimal> quote = () => 10m;
quote += () => 25m;
Console.WriteLine($"Func returns: {quote()}"); // last wins
static void SendEmail(string id) =>
Console.WriteLine($"Email: {id} confirmed");
static void SendText(string id) =>
Console.WriteLine($"Text: {id} confirmed");
Output:
Email: order 1047 confirmed
Text: order 1047 confirmed
Warehouse: pick order 1047
Handlers: 3
Email: order 1048 confirmed
Warehouse: pick order 1048
Func returns: 25

GetInvocationList() reported 3 handlers. After -= SendText, order 1048 got only the email and warehouse lines.
I declared notify as nullable because -= returns null once the last handler is gone. That’s why the second call uses ?.Invoke.
The Func part shows the catch: both lambdas ran, but the caller only got 25, the last method’s return value. Multicast makes sense for void work, not for collecting results.
Pro Tip: if one handler throws, the remaining ones never run and the exception reaches the caller. Loop over GetInvocationList() with a try catch block when every handler must get its turn.
Lambda Expressions and Anonymous Methods as Delegates
Lambdas are the normal way to create delegates today. The older delegate (decimal s) { ... } syntax still compiles; anonymous methods in C# covers it in detail.
Newer C# versions added a few lambda features worth knowing, all on the same $80 cart:
using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");
// Anonymous method (C# 2): same job as a lambda, older syntax
Func<decimal, decimal> save10 = delegate (decimal s) { return s * 0.90m; };
// Natural type (C# 10): var works once the parameter type is written
var addTax = (decimal s) => Math.Round(s * 1.0825m, 2);
Console.WriteLine(addTax.GetType().Name);
// static lambda (C# 9): can't capture locals by accident
Func<decimal, decimal> shipping = static s => s >= 100m ? 0m : 4.99m;
// Explicit return type + default parameter value (C# 10 / C# 12)
var label = string (decimal s, string currency = "USD") => $"{s:F2} {currency}";
Console.WriteLine(label(addTax(save10(80m))));
Console.WriteLine($"Shipping on $64.25: {shipping(64.25m):C}");
Output:
Func`2
77.94 USD
Shipping on $64.25: $4.99
| Feature | Version | What it does |
|---|---|---|
| Anonymous method | C# 2 | delegate keyword inline; replaced by lambdas |
static lambda | C# 9 | Can’t capture locals or this, so nothing gets captured by accident |
| Natural type | C# 10 | var infers Func or Action when parameter types are written |
| Explicit return type | C# 10 | string (decimal s) => ... when the return type can’t be inferred |
| Default parameter values | C# 12 | string currency = "USD" inside the lambda’s parameter list |
The run printed Func`2 for addTax, because the compiler picked Func<decimal, decimal>. The chained call printed 77.94 USD, and shipping on a $64.25 cart cost $4.99.
A lambda with a default parameter doesn’t map to Func, so the compiler synthesizes its own delegate type for label. C# 14 also lets you add modifiers such as ref without writing parameter types; see what is new in C# 14.
Delegates vs Events in C#: What’s the Difference?
An event is a delegate field with a guard around it. Outside the class, code can only += and -= handlers; it can’t call the event or replace the whole list. Events in C# walks through the full publisher and subscriber pattern.
| Public delegate field | Event | |
|---|---|---|
Subscribe with += / -= | Yes | Yes |
Overwrite with = | Yes, any caller can wipe every handler | No (CS0070) |
| Invoke from outside the class | Yes | No, only the owning class raises it |
| Use it for | Callbacks you pass into a method | Notifications to any number of listeners |
My rule: if a method needs behavior to do its job, take a delegate parameter. If an object announces that something happened, expose an event.
Delegate or Interface: Which Should You Use?
Both let a caller plug in behavior. An interface in C# is the better fit once the behavior grows past one method:
| Pick a delegate when | Pick an interface when |
|---|---|
| The contract is one method | Several related methods belong together |
| Callers pass a lambda or a static method | The implementation needs its own state and setup |
| You may combine handlers (multicast) | A class implements it once, as part of its identity |
| You use it as an event | You resolve it through dependency injection |
In the store app, DiscountRule stays a delegate. A payment gateway with Authorize, Capture and Refund becomes an interface. The OOP concepts in C# guide covers where interfaces fit in a class design.
Common Delegate Errors and How to Fix Them
I broke the store code on purpose to show the three compiler errors I see most with delegates:
DiscountRule rule = HalfOff; // int, not decimal
var tax = s => s * 1.0825m; // no type to infer
var cart = new Cart();
cart.Changed = null; // event, not field
static decimal HalfOff(int subtotal) => subtotal / 2m;
delegate decimal DiscountRule(decimal subtotal);
class Cart
{
public event Action? Changed;
public void Add() => Changed?.Invoke();
}
Build output:
C:\DotNetDemo\DelegatesDemo\Errors.cs(1,21): error CS0123: No overload for 'HalfOff' matches delegate 'DiscountRule'
C:\DotNetDemo\DelegatesDemo\Errors.cs(2,11): error CS8917: The delegate type could not be inferred.
C:\DotNetDemo\DelegatesDemo\Errors.cs(4,6): error CS0070: The event 'Cart.Changed' can only appear on the left hand side of += or -= (except when used from within the type 'Cart')
The build failed. Fix the build errors and run again.

| Error | Cause | Fix |
|---|---|---|
| CS0123 | The method’s parameters don’t match the delegate | Change int to decimal, or wrap it in a lambda |
| CS8917 | var on a lambda without parameter types | Write (decimal s) => or declare Func<decimal, decimal> |
| CS0070 | Assigning or invoking an event outside its class | Use += / -=, or raise it inside the class |
NullReferenceException | Invoking a delegate that has no methods | Call rule?.Invoke(s) |
An empty delegate variable is just null, which is why the Cart class raises its event with ?.Invoke. Checking for null values in C# covers the other null patterns.
Which Delegate Type Should You Use?
Funcwhen the code returns a value,Actionwhen it doesn’t.Predicateonly forListandArraymethods that ask for one.- A named custom delegate when the name explains the role or you need
ref/out. - An event when other objects subscribe to notifications.
- An interface when the behavior is several methods with shared state.
Once delegates click, these four posts on the store’s other pieces are next:
- See why overloads and delegates treat the return type differently
- Compare abstract classes with interfaces for pluggable behavior
- Check what changed in .NET 10 before you upgrade
- Set up VS Code to run these delegate samples
Frequently Asked Questions
What is a delegate in C# in simple terms?
A delegate is a type that holds a reference to a method with a specific parameter list and return type. You can store it in a variable, pass it to another method and call it later, which makes it ideal for callbacks.
What is the difference between Func and Action in C#?
Func returns a value, and its last type argument is the return type. Action returns nothing. Both have versions with up to 16 parameters.
What does a multicast delegate return?
It calls every method in its invocation list in order and returns the value of the last one. The earlier return values are lost, so multicast is best for methods that return void.
Are lambdas and delegates the same thing?
No. A lambda is syntax for an anonymous function, and the compiler converts it to a delegate type such as Func or Action (or to an expression tree). The delegate is the object you call.
Why use a delegate instead of calling the method directly?
A delegate lets the caller decide which method runs, so code like a checkout or a sort routine stays the same while the rule changes. The Using Delegates guide on Microsoft Learn shows the same callback pattern.
Is an event a delegate in C#?
An event is built on a delegate type, but it limits outside code to += and -=. Only the declaring class can raise it or reset its handler list.
Bijay Kumar is a renowned software engineer, accomplished author, and distinguished Microsoft Most Valuable Professional (MVP) specializing in SharePoint. With a rich professional background spanning over 15 years, Bijay has established himself as an authority in the field of information technology. He possesses unparalleled expertise in multiple programming languages and technologies such as ASP.NET, ASP.NET MVC, C#.NET, and SharePoint, which has enabled him to develop innovative and cutting-edge solutions for clients across the globe. Read more…