Action, Func and Predicate — Built-In Delegate Types

Rather than declaring a new delegate type for every callback signature, .NET provides three built-in generic delegate families that cover virtually every case: Action<T> (void return, up to 16 parameters), Func<T, TResult> (non-void return, up to 16 input parameters), and Predicate<T> (returns bool, exactly one parameter). These types are used throughout the .NET BCL and ASP.NET Core — LINQ operators accept Func<T, bool>, middleware accepts Func<HttpContext, Task>, and DI accepts Func<IServiceProvider, T> factory delegates.

Action — Void Callbacks

// Action — void return, 0 to 16 parameters
Action              doNothing  = () => { };
Action<string>     log        = msg => Console.WriteLine(msg);
Action<int, int>   printSum   = (a, b) => Console.WriteLine(a + b);
Action<string, int, bool> complex = (s, n, b) => DoComplexWork(s, n, b);

log("Application started");     // "Application started"
printSum(10, 20);               // 30

// Action as a callback parameter
public void ProcessAll(IEnumerable<Post> posts, Action<Post> onEach)
{
    foreach (var post in posts)
        onEach(post);
}

ProcessAll(posts, p => Console.WriteLine(p.Title));
ProcessAll(posts, p => _cache.Invalidate(p.Id));
Note: Action and Func are declared as generic types in the System namespace. Action<T1, T2, ..., T16> represents a method with up to 16 parameters that returns void. Func<T1, T2, ..., T16, TResult> represents a method with up to 16 parameters that returns TResult — the return type is always the last type argument. Func<string, int> means a function that takes a string and returns an int. Func<int, int, int> takes two ints and returns an int.
Tip: Higher-order functions — functions that take other functions as parameters — are one of the most powerful patterns in C#. They appear throughout ASP.NET Core: app.Use(async (ctx, next) => { ... await next(ctx); ... }) — the middleware delegate accepts a RequestDelegate (which is essentially a Func<HttpContext, Task>). Building your own higher-order functions with Action/Func parameters enables flexible, reusable code without inheritance — a component of the “prefer composition over inheritance” principle.
Warning: Predicate<T> and Func<T, bool> are compatible in terms of signature but are different delegate types — you cannot pass one where the other is expected without an explicit conversion. List<T>.FindAll(Predicate<T>) requires a Predicate<T>, while LINQ’s Where(Func<T, bool>) requires a Func<T, bool>. A lambda like p => p.IsPublished is compatible with both, but a stored delegate variable of type Predicate<Post> cannot be passed to Where() directly.

Func — Functions with Return Values

// Func — returns a value, 0 to 16 input parameters + 1 return type
Func<int>           random   = () => new Random().Next(100);
Func<string, int>   length   = s => s.Length;
Func<int, int, int> multiply = (a, b) => a * b;
Func<Post, PostDto> toDto    = p => new PostDto { Id = p.Id, Title = p.Title };

Console.WriteLine(length("Hello"));      // 5
Console.WriteLine(multiply(6, 7));       // 42

// Higher-order function using Func
public static IEnumerable<TResult> Transform<T, TResult>(
    IEnumerable<T> source,
    Func<T, TResult> selector)
    => source.Select(selector);

var titles = Transform(posts, p => p.Title);   // IEnumerable<string>
var dtos   = Transform(posts, toDto);           // IEnumerable<PostDto>

// Func as a factory (used in DI and caching)
Func<IServiceProvider, IEmailSender> emailFactory =
    sp => new SmtpEmailSender(sp.GetRequiredService<SmtpSettings>());

builder.Services.AddScoped<IEmailSender>(emailFactory);

Pipeline Pattern with Func

// Chain Func delegates into a processing pipeline
public class Pipeline<T>
{
    private readonly List<Func<T, T>> _steps = new();

    public Pipeline<T> AddStep(Func<T, T> step) { _steps.Add(step); return this; }

    public T Execute(T input)
        => _steps.Aggregate(input, (current, step) => step(current));
}

// Use the pipeline to process post content
var pipeline = new Pipeline<string>()
    .AddStep(s => s.Trim())
    .AddStep(s => s.ToLowerInvariant())
    .AddStep(s => s.Replace(" ", "-"))
    .AddStep(s => s.Length > 50 ? s[..50] : s);

string slug = pipeline.Execute("  Hello World, This is a Long Title!  ");
// "hello-world,-this-is-a-long-title!"

Common Mistakes

Mistake 1 — Confusing Func parameter order (return type is LAST)

❌ Wrong — misreading the type signature:

Func<int, string> f;   // takes string, returns int — WRONG mental model!

✅ Correct mental model: all type parameters before the last are inputs; the last is the output:

Func<int, string> f = n => n.ToString();   // takes int, returns string ✓

Mistake 2 — Using Predicate<T> where Func<T, bool> is required (or vice versa)

Create lambda expressions directly at the call site rather than storing as a typed delegate to avoid type incompatibility issues.

🧠 Test Yourself

What is the type of a delegate that takes a Post and a string and returns a bool?