Last Updated: May 22, 2026
The .NET Base Class Library ships three families of generic delegate types: Action, Func, and Predicate<T>. Together they cover almost every callback shape a method might accept, which is why most C# code never needs to declare a custom delegate type. This lesson walks through what each family does, when to use each one, and how to design methods that take them as parameters.
A delegate type is a contract for "a method that takes these parameters and returns this result." If every library author declared their own delegate types for every callback shape, the type system would fill up with PriceCalculator, OrderFilter, ProductFormatter, DiscountCheck, and a hundred others that mean the same thing structurally. Callers would have to convert between them, even with matching underlying signatures, because in C# two delegate types are not interchangeable just because their signatures line up.
The BCL solves this by defining a small set of generic delegate types that cover the common shapes once. Any method that wants a "function that takes an int and returns a string" can ask for Func<int, string>, and any caller with such a method (or a lambda that produces one) can pass it directly. The types are part of the framework, so they're universally recognized, and every team that uses them gets the same vocabulary.
Three families cover the field:
Action, Action<T>, all the way up to Action<T1, T2, ..., T16>. These describe methods that return void. Use them for callbacks that do something with the inputs but don't compute a result.Func<TResult>, Func<T, TResult>, up to Func<T1, ..., T16, TResult>. These describe methods that return a value. The last type parameter is always the return type.Predicate<T>. A single non-generic family member that means "a method taking a T and returning a bool." It exists for one purpose: filtering.Custom delegate declarations (covered in lesson 01) are still useful when the type name itself needs to carry meaning, especially for public APIs where Func<Order, decimal, bool> is less self-documenting than OrderEligibilityCheck. For most internal code, the BCL delegates are the standard.
Throughout this lesson the same E-Commerce shapes appear: products, carts, orders, customers, discounts. The examples favor short lambdas because that's how these delegates are usually supplied at the call site, but the syntax details belong to lesson 04. For now, treat x => x.Price > 100m as "a function that takes an x and returns whether its price is over 100."
The picture is straightforward: the BCL provides three shapes, a caller hands one to a method, and the method invokes it. The rest of this lesson covers choosing the right shape and using it correctly.
One detail before going further: these delegate types are declared in the System namespace and ship as part of mscorlib / System.Runtime, so no extra using directive or NuGet package is required. They're as available as int and string. They appear throughout the framework, from Task.Run(Action) to Lazy<T>(Func<T>) to List<T>.ForEach(Action<T>). Learning these three families pays off across the entire .NET ecosystem.
Action and Action<T> (Void-Returning Callbacks)Action describes a method that takes no parameters and returns nothing. Action<T> takes one parameter of type T and returns nothing. Action<T1, T2> takes two parameters. The pattern continues up through sixteen type parameters, which is many more than you should ever need in practice.
Use Action when the callback is doing something side-effecting: writing to a log, sending a notification, recording a metric, updating a UI, raising an alert. The method does not need a return value from the callback because the work the callback does is the whole point.
A typical use is a logger callback. A class that processes orders might not want to take a hard dependency on a specific logging library; instead, it accepts an Action<string> and calls it whenever something interesting happens. The caller decides whether that string ends up in a file, on the console, or in a centralized logging service.
OrderProcessor doesn't know or care what the logger does with the strings. It just knows the shape: "give me something I can hand a string to." The same processor could be wired to a file writer, an in-memory list (useful in tests), or a no-op _ => { } for production paths where logging would be too verbose.
Two-parameter actions show up just as often. A cart that wants to notify a UI when items are added might take an Action<string, int> representing "product name, new count."
The cart fires the callback after every successful add. The caller decides what "notify" means. In a real application, that callback might push a message onto a UI event bus, increment a counter, or queue a confirmation email.
A parameterless Action is rarer but does show up, usually for "do this thing once" hooks: a cache that takes an Action to call when it's invalidated, or a shutdown handler that takes an Action to run when a process is closing.
The Action has no inputs because nothing about the invalidation event needs to be passed to the handler.
Action<T> is the C# spelling of "consumer." The method that takes the delegate produces the values, and the delegate consumes them. Anywhere a method needs "I'll generate these things, you decide what to do with them," Action is the type. Loggers, observers, progress reporters, and event-like callbacks all fit. When drafting a method that takes a list and a "do something" function, use Action<T> first and only switch to Func<T, TResult> if the method needs the function's return value.
A lambda that captures a local variable may allocate a small closure object plus a delegate instance to bind to it. A lambda with no captures is cached after its first use, so the allocation happens once. Inside a hot loop, capturing variables and rebuilding lambdas every iteration is one of the easier ways to make a tight loop allocate more than necessary.
Func<...,TResult> (Value-Returning Callbacks)Func is the same idea as Action, except the method returns a value. The convention is that the last type parameter is the return type, and every earlier one is an input. So Func<TResult> is a parameterless function returning TResult. Func<T, TResult> takes a T and returns a TResult. Func<T1, T2, TResult> takes two inputs and returns. The family scales up to Func<T1, ..., T16, TResult>, again far more than you should normally need.
Use Func whenever the caller's job is to compute something on demand. A common shape is a price calculator: a method that knows about a cart but lets the caller plug in how the cart's total is computed.
CalculateTotal doesn't know how each line should be priced. It takes a Func<CartItem, decimal> and trusts the caller to define the rule. One caller computes a plain subtotal; another applies tax. The cart didn't need a second method, a subclass, or a configuration flag. The callback shape carries the variation.
A two-parameter Func is just as common. Consider a cart that applies a discount depending on both the item and a coupon. The callback gets both, returns the discounted line total.
The Func<CartItem, Coupon, decimal> means "given an item and a coupon, return a decimal." The cart calls it for each item with the same coupon. The caller decides whether the discount applies per item, only to certain products, or with a cap.
A parameterless Func<TResult> is useful for lazy values: things the caller might or might not need, computed only when requested. A common shape is a default-value provider on a dictionary lookup.
For product 1, the dictionary has a hit, so the fallback delegate is never invoked. The side-effecting Console.WriteLine inside the fallback doesn't run. For product 9, the lookup misses and the fallback runs, producing both its log line and its return value. Passing a Func<TResult> instead of a precomputed value defers the cost until it's needed.
Func types can return any type, including reference types, value types, generics, and even other delegates. A Func<int, Func<int, int>> is a function that takes an int and returns a function that takes an int and returns an int. This is the standard shape for partial application and currying.
applyDiscount is a factory: pass it a percentage and it returns a function that applies that percentage. Each returned function captures its own copy of the percent value, so tenPercentOff and twentyPercentOff are independent. Func types compose, and the language lets functions be treated as values just like any other type.
A second common pattern is using Func<T, TKey> for projection: a callback that pulls a key out of an item, used by sorts, groupings, and lookups. The BCL has many APIs following this shape, including OrderBy, GroupBy, ToDictionary, and ToLookup from LINQ.
IndexBy doesn't know which field of Customer should be the key. The caller decides with a Func<Customer, TKey>, and the type system follows along: passing c => c.Id makes TKey infer as int, while passing c => c.City makes it infer as string. One method, many indexes, no extra overloads needed.
Predicate<T> and Why Func<T, bool> Usually WinsPredicate<T> is the BCL's name for "a function that takes a T and returns a bool." It's defined as if you'd written public delegate bool Predicate<T>(T obj);. The intent is filtering: given an item, decide whether it passes some test.
The type has been around since .NET 2.0, when generics first arrived and the BCL needed a way to express things like List<T>.Find and Array.FindAll. Those APIs still take Predicate<T> today.
List<T>.Find returns the first element matching the predicate, or the default value (here, null) if none match. FindAll returns every element matching it. Both take a Predicate<T>.
Structurally, Predicate<T> and Func<T, bool> are interchangeable. They have the same signature: one parameter of type T, one return value of type bool. A lambda like p => p.Stock > 0 could be assigned to either.
The lambda body is identical. What's not interchangeable is the type: a Predicate<Product> and a Func<Product, bool> are different delegate types from the C# language's point of view, even though they describe the same shape. You cannot assign one to a variable of the other type without going through a conversion. The C# compiler will create a new delegate that wraps the call:
Two near-identical types covering the same ground would be a problem if both were used widely. In modern .NET, almost everything new uses Func<T, bool>. LINQ, which lives in System.Linq and is the common reason a C# developer touches a "filter" callback, accepts Func<T, bool> everywhere. ASP.NET Core, Entity Framework, and most other framework APIs follow suit.
Predicate<T> appears mainly in older corners of the BCL: List<T>.Find, Array.Exists, List<T>.RemoveAll, and a few others. For new APIs, default to Func<T, bool> because it composes naturally with everything else and callers won't have to convert between two effectively equivalent types.
Compare the two side by side:
| Property | Predicate<T> | Func<T, bool> |
|---|---|---|
| Defined as | public delegate bool Predicate<T>(T obj); | public delegate TResult Func<T, TResult>(T arg); with TResult = bool |
| Lives in | System (since .NET 2.0) | System (since .NET 3.5) |
| Used by | List<T>.Find, FindAll, RemoveAll, Array.Exists, etc. | LINQ (Where, Any, All, Count), most modern APIs |
| Composes with LINQ? | No, requires conversion | Yes, directly |
| Recommended for new code? | Only when matching an existing API that requires it | Yes |
A small example shows the friction. Filtering a list using one of those older methods and then doing a LINQ operation on the result:
FindAll requires a Predicate<Product>, but the variable is a Func<Product, bool>, so the call site needs an explicit wrap. Where takes the Func<Product, bool> directly. The behavior is the same; one is more ceremonious.
Wrapping a Func<T, bool> in a new Predicate<T>(...) allocates a new delegate instance. In a method called once, this is fine. Inside a loop that filters many collections, it adds up. The fix is to declare the variable as Predicate<T> from the start when the API requires that type, or to use the LINQ method (Where, Any, All) that takes Func<T, bool> directly.
A table makes the three families easy to scan side by side. Same purpose, different shapes.
| Family | Return type | Parameters | Typical use | Example signature |
|---|---|---|---|---|
Action | void | none | Do-something hook, one-time callback | Action for a cache invalidation handler |
Action<T> ... Action<T1,...,T16> | void | 1-16 inputs | Side-effecting callback (logging, notification, UI update) | Action<string> for a logger |
Func<TResult> | TResult | none | Lazy producer, deferred default | Func<decimal> for a fallback price |
Func<T,TResult> ... Func<T1,...,T16,TResult> | TResult | 1-16 inputs | Compute a value from inputs (price, projection, selector) | Func<CartItem, decimal> for a line-total rule |
Predicate<T> | bool | one input | Filtering on older BCL APIs (List<T>.Find, Array.Exists) | Predicate<Product> for in-stock check |
The decision tree is short:
Action.Func, with the return type last.bool and the API specifically requires Predicate<T>? Use it. Otherwise, use Func<T, bool>.The "only for old APIs" branch is important. Predicate<T> is not deprecated, and there's no reason to remove it from existing code that uses it. The recommendation applies only to defaults for new methods that accept a callback.
The three families look distinct on paper, and they really are. In practice, the choice falls out of one question: what does the callback need to give back to the calling method?
If the answer is "nothing, just do the thing," it's an Action. The calling method invokes the delegate and moves on. Logging, notifications, and side effects all fit. If the callback needs to compute something the method will use (a price, a key, a sort order), it's a Func. The calling method takes the return value and feeds it into whatever it's doing next. If the callback tests an item for inclusion, it's still a Func<T, bool> for new code, with Predicate<T> reserved for matching an existing BCL signature.
A small helper shows the design choice in code. For a WhereStockExists method that filters products, the parameter can be spelled three different ways, and only one is correct for new code.
The method takes Func<Product, bool>. The same shape as LINQ's Where, so a caller can compose them naturally: catalog.WhereStockExists(predicate).Select(...) works without any conversion. With Predicate<Product> as the parameter, the method would still work, but every LINQ-using caller would have to wrap their lambda in a new Predicate<Product>(...), and the method couldn't easily be implemented in terms of Where.
A second example pairs an Action and a Func in the same method. A ProcessOrders helper might log progress (an Action<string>) and price each order (a Func<Order, decimal>), keeping the policy decisions outside the method.
Two different callback shapes serving two different jobs. The Func<Order, decimal> carries the pricing rule, the Action<string> carries the reporting rule, and Process itself doesn't know or care how either one is implemented. Replacing either rule is a matter of passing a different lambda; the method stays put.
A subtler design rule: accept only the simplest callback type the method actually needs. For filtering, accept Func<T, bool>, not Func<T, object> and then check truthiness. For projecting, accept Func<T, TResult> with a specific TResult, not Func<T, object>. The narrower the type, the more the compiler can help callers, and the harder it is to pass nonsense in.
The mirror rule for callers: when supplying a callback used in many places, give it a name. A field, a local variable, or a method group reference is more readable than a lambda repeated five times.
The second form pays a one-time cost (a single named variable) and removes three duplicated lambdas. If the rule changes, change it once. The naming also serves as inline documentation: a reader sees isPurchasable and understands the intent without parsing the lambda body.
The full mechanics of how lambdas turn into delegate instances (and when each form allocates) belong to the _Lambda Expressions_ lesson. The takeaway here is that Action, Func, and Predicate<T> are the vocabulary for expressing callback shapes, and most of the BCL is built on this vocabulary. Picking the right one is a matter of asking what the callback returns and using the family that matches.