Default Lambda Parameters: Optional Arguments in Anonymous Functions

C# 12 closes a parity gap between lambda expressions and regular methods by allowing default parameter values and params arrays in lambdas. It is a small feature with a clear use case: making lambdas more flexible without resorting to overloads or wrapper methods.

The Feature

You can now specify default values for lambda parameters, just as you would with a method:

Example.cs
var greet = (string name, string greeting = "Hello") => $"{greeting}, {name}!";

Console.WriteLine(greet("Alice"));           // "Hello, Alice!"
Console.WriteLine(greet("Bob", "Welcome"));  // "Welcome, Bob!"

This works with any delegate type that supports optional parameters, including Func<> and custom delegates.

Before and After

Previously, achieving optional parameters in a lambda required workarounds. The most common was to accept a nullable parameter and apply the default manually:

Example.cs
// Before: manual default handling
var greet = (string name, string? greeting) =>
    $"{greeting ?? "Hello"}, {name}!";

greet("Alice", null); // Had to pass null explicitly

Or you would define two separate lambdas:

Example.cs
var greetDefault = (string name) => $"Hello, {name}!";
var greetCustom = (string name, string greeting) => $"{greeting}, {name}!";

Both approaches are clumsy. Default parameters fix this cleanly.

Params in Lambdas

Alongside default values, lambdas now support params:

Example.cs
var joinWords = (params string[] words) => string.Join(" ", words);

Console.WriteLine(joinWords("Hello", "world"));   // "Hello world"
Console.WriteLine(joinWords("one", "two", "three")); // "one two three"

Combined with C# 13's params collections, this becomes even more flexible as params can target spans and other collection types.

Practical Use Cases

Configuration Callbacks

Default parameters make configuration lambdas more ergonomic:

Example.cs
var configure = (string host, int port = 5432, bool useSsl = true) =>
    new NpgsqlConnectionStringBuilder
    {
        Host = host,
        Port = port,
        SslMode = useSsl ? SslMode.Require : SslMode.Disable
    };

var devConfig = configure("localhost");
var prodConfig = configure("db.prod.example.com", 5433, true);

Pipeline Builders

When building processing pipelines with lambdas, defaults reduce the number of arguments callers need to supply:

Example.cs
var retry = (Func<Task> action, int maxAttempts = 3, TimeSpan? delay = null) =>
    async () =>
    {
        var wait = delay ?? TimeSpan.FromSeconds(1);
        for (int i = 0; i < maxAttempts; i++)
        {
            try { await action(); return; }
            catch when (i < maxAttempts - 1)
            {
                await Task.Delay(wait);
            }
        }
    };

var resilientCall = retry(() => httpClient.GetAsync("https://example.com"));
var customRetry = retry(() => httpClient.GetAsync("https://example.com"), maxAttempts: 5);

Minimal API Handlers

In ASP.NET minimal APIs where handlers are lambdas, default parameters let you provide sensible defaults for query parameters:

Example.cs
app.MapGet("/products", (int page = 1, int pageSize = 20, string? category = null) =>
{
    var query = db.Products.AsQueryable();

    if (category is not null)
        query = query.Where(p => p.Category == category);

    return query
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();
});

Delegate Type Inference

When a lambda has default parameters, the compiler generates a delegate type that includes those defaults. This means the defaults are preserved when the lambda is invoked through the delegate:

Example.cs
var format = (decimal amount, string currency = "GBP", int decimals = 2) =>
    $"{currency} {amount.ToString($"F{decimals}")}";

// The delegate retains the defaults
Console.WriteLine(format(42.5m));            // "GBP 42.50"
Console.WriteLine(format(42.5m, "USD"));     // "USD 42.50"
Console.WriteLine(format(42.5m, "EUR", 4));  // "EUR 42.5000"

Limitations

Default lambda parameters have the same restrictions as default method parameters: the default value must be a compile-time constant (or default for reference types and value types). You cannot use a method call or computed expression as a default.

Also, if you assign a lambda with default parameters to a specific delegate type like Func<string, string>, the default is lost because Func does not carry parameter metadata. The defaults are preserved only when the compiler infers the delegate type or when using a custom delegate that declares the defaults.

Verdict

This is a quality-of-life improvement that makes lambdas first-class citizens for scenarios that previously required named methods. If you are writing lambdas that would benefit from optional parameters — and minimal API developers certainly are — this is a welcome simplification.