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:
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:
// 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:
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:
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:
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:
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:
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:
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.