The built-in generic delegates Func, Action and Predicate: signatures, variance, where the BCL uses each, delegate type incompatibility and choosing custom delegates or expression trees.
Func is a delegate that returns a value, so Func<int, string> takes an int and returns a string. Action returns void, and Predicate<T> is a delegate that takes a T and returns bool. Older APIs like List<T>.FindAll use it, while LINQ uses Func<T, bool>. Even with identical signatures they are different delegate types, so you cannot pass a Func<T, bool> variable as a Predicate<T> without wrapping it. A lambda converts to either. I declare my own delegate only when I need ref or out parameters or a meaningful name.
Func, Action and Predicate are built-in generic delegate types, so you rarely need to declare your own:
Action<...> — takes 0–16 parameters, returns void.Func<..., TResult> — takes 0–16 parameters, returns TResult (the last type argument).Predicate<T> — takes one T, returns bool (an older, specialised delegate from .NET 2.0).Then the subtle point: delegate types are nominal, so a Func<T, bool> isn't convertible to a Predicate<T> even though the signatures match.
Action log = () => Console.WriteLine("Started");
Action<string, LogLevel> write = (msg, level) => logger.Log(level, msg);
Func<DateTimeOffset> now = () => DateTimeOffset.UtcNow;
Func<Order, decimal> total = o => o.Lines.Sum(l => l.Amount);
Func<string, CancellationToken, Task<Customer?>> load = repo.FindByEmailAsync;
Predicate<Order> isOverdue = o => o.DueDate < today;
List<Order> overdue = orders.FindAll(isOverdue); // List<T>.FindAll/RemoveAll/Exists take Predicate<T>
Action: callbacks and commands where you don't need a result (Parallel.ForEach, Task.Run(Action), configuration callbacks like services.Configure<Options>(o => ...)).Func: factories, projections, selectors, async callbacks (Func<Task>), and anything returning a value — LINQ uses Func everywhere (Where(Func<T,bool>), Select(Func<T,TResult>)).Predicate<T>: mostly when calling older BCL APIs that require it (List<T>.FindAll, RemoveAll, Array.Find, Array.Exists). In new APIs, prefer Func<T, bool> for consistency with LINQ.Related built-ins: Comparison<T> (List<T>.Sort), Converter<TInput, TOutput> (ConvertAll), EventHandler<T> for events.
Func<T, bool> can't be passed as Predicate<T>Func<Order, bool> f = o => o.Total > 100;
orders.FindAll(f); // CS1503: cannot convert Func<Order,bool> to Predicate<Order>
Delegate types are distinct named types (each is a class deriving from MulticastDelegate). C# has no structural conversion between delegate types with identical signatures — just as two different classes with the same members aren't interchangeable.
Fixes:
orders.FindAll(new Predicate<Order>(f)); // wrap (creates a new delegate pointing at f.Invoke)
orders.FindAll(f.Invoke); // method group conversion of Invoke
orders.FindAll(o => f(o)); // lambda wrapper
orders.Where(f).ToList(); // or use LINQ, which takes Func
A lambda or method group can convert to either type directly — the problem only arises once it's already stored in a variable of one delegate type.
ref, out, in parameters, params, or pointer types — Func/Action can't express them: public delegate bool TryParser<T>(ReadOnlySpan<char> s, out T value);delegate decimal DiscountRule(Order o)) can carry XML docs and a meaningful name in APIs, especially public library APIs.EventHandler<T> is the convention).[UnmanagedFunctionPointer] (or function pointers).Func/Action with allows ref struct, reducing this need).Otherwise prefer Func/Action — every developer knows them, and they compose with LINQ and the BCL.
A validation library defined its own delegate bool Rule<T>(T value) and every consumer converted between Rule<T>, Predicate<T> and Func<T,bool> with wrapper lambdas — conversions sprinkled everywhere and occasional confusion about which overload to call. Standardising on Func<T, bool> in the public API (keeping Rule<T> as an obsolete overload during migration) removed the wrappers and made rules usable directly with LINQ.
Checks fluency with the built-in delegate types and awareness of the Func versus Expression trap.
Green flags: correct signatures; knows the last type argument is the result; explains nominal typing; brings up Expression<Func<>> with EF Core; reasons about custom delegates.
Red flags: thinks Predicate and Func<T,bool> are interchangeable variables; unaware of expression trees.
public delegate void Action();
public delegate void Action<in T>(T obj);
// ... up to Action<in T1, ..., in T16>
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
// ... up to 16 inputs
public delegate bool Predicate<in T>(T obj);
All are variant: inputs in (contravariant), result out (covariant). In .NET 9 many were annotated with allows ref struct so spans can be type arguments.
Func<string> → Func<object>).new Predicate<T>(func) creates a delegate targeting func.Invoke, adding one level of indirection.Since C# 10, lambdas have a natural type: var f = (Order o) => o.Total > 100; infers Func<Order, bool> — another reason Func is the de-facto standard.
Func<T, bool> lambdas can also be converted to Expression<Func<T, bool>> (used by IQueryable/EF Core). A variable of type Func<T,bool> can't become an expression tree — passing a compiled Func to EF Core's Where binds to Enumerable.Where (in-memory) instead of Queryable.Where, loading the whole table. This is a common EF performance bug.
new Predicate<T>(f)) adds an extra call and allocation.| API style | Prefer |
|---|---|
| LINQ-like filtering/projection | Func<T, bool>, Func<T, TResult> |
| Callback without result | Action<T> |
| Async callback | Func<T, CancellationToken, Task> |
Try-pattern callbacks with out |
Custom delegate |
| Public library extension point with rich semantics | Interface (multiple methods) or named delegate with docs |
Func<int,bool> and pass it to List<int>.FindAll; fix the error three ways.var f = (string s) => s.Length; and inspect its type.delegate bool TryParser<T>(string s, out T value) and use it with int.TryParse.Func<Order,bool> variable to an EF Core query's Where and inspect the generated SQL (or lack of a WHERE clause).using System.Linq.Expressions;
namespace Delegates.BuiltIn;
public sealed record Customer(int Id, string Name, bool IsActive, decimal Balance);
public delegate bool TryParser<T>(string input, out T value); // Func cannot express out
public static class Program
{
public static void Main()
{
List<Customer> customers =
[
new(1, "Ava", true, 120m),
new(2, "Noah", false, 0m),
new(3, "Mia", true, -15m),
];
Action<string> log = message => Console.WriteLine($"[log] {message}");
Func<Customer, string> label = c => $"{c.Id}:{c.Name}";
Predicate<Customer> inDebt = c => c.Balance < 0;
Func<Customer, bool> isActive = c => c.IsActive;
log(string.Join(", ", customers.Where(isActive).Select(label)));
log($"In debt: {customers.FindAll(inDebt).Count}");
// customers.FindAll(isActive); // CS1503: Func<Customer,bool> is not Predicate<Customer>
log($"Active: {customers.FindAll(isActive.Invoke).Count}"); // method group conversion
log($"Active: {customers.FindAll(new Predicate<Customer>(isActive)).Count}");
Func<object, string> describe = o => o.ToString() ?? "";
Func<Customer, object> asObject = describe; // variance: in T, out TResult
log(asObject(customers[0]).ToString()!);
Expression<Func<Customer, bool>> activeExpr = c => c.IsActive; // data, for IQueryable providers
log(activeExpr.Body.ToString());
TryParser<int> parser = int.TryParse;
log(parser("42", out var n) ? $"parsed {n}" : "failed");
}
}
### 1. Expecting structural delegate conversion
**What happens:** passing a `Func
The senior distinction is not memorising signatures; it is knowing when the delegate is code and when it must be data. Func<T, bool> versus Expression<Func<T, bool>> decides whether a query runs in SQL Server or in your pod's memory, and I have seen that one type cause production outages. I default to Func/Action in new APIs for interoperability, and use named delegates where names or ref parameters genuinely add clarity, as ASP.NET Core does with RequestDelegate.
A lambda is an anonymous function, either an expression like x => x * 2 or a statement block. It has no type until it is converted, either to a delegate, which is compiled code, or to an Expression<Func<...>>, which is a data structure EF Core can turn into SQL. Since C# 10 a lambda can have a natural type, so var f = (int x) => x * 2 is a Func<int,int>. When it captures variables, the compiler hoists them into a closure class, which allocates. static lambdas forbid capture. Newer additions are attributes and return types (C# 10), default parameters (C# 12), and C# 14 parameter modifiers like out without writing types.
A delegate is a type-safe reference to a method. The delegate type defines the signature, and a delegate instance is an object that holds the method and, for instance methods, the target object, so calling Invoke calls that method on that object. Delegates are immutable reference types that derive from MulticastDelegate, and two delegates are equal if they point at the same method on the same target. Creating one allocates, though the compiler caches non-capturing lambdas. I use Func and Action for most callbacks, and an interface when the contract has more than one method.
Last updated 30 Oct 2026.