June 17th, 2024

Refactor your code with default lambda parameters

David Pine
Senior Content Developer

This is the last post in a series of four, exploring various C# 12 features. In this post, we’ll explore the “default lambda parameters” feature, enabling developers to express default parameter values in lambdas. This series has covered a lot of ground:

  1. Refactor your C# code with primary constructors
  2. Refactor your C# code with collection expressions
  3. Refactor your C# code by aliasing any type
  4. Refactor your C# code to use default lambda parameters (this post)

These features are part of our ongoing effort to enhance code readability and maintainability. Let’s explore them in detail!

Default Lambda Parameters 🧮

Default lambda parameters are a new feature in C# 12 that allows developers to express default parameter values in lambdas. This feature is a natural extension of the existing default parameter feature in C# methods.

Before C# 12 🕰️

Before C# 12, when you’d define a lambda expression that needed to provide some sort of default behavior you had to resort to using the null-coalescing operator (??) or the conditional operator (?:). Consider the following example:

var IncrementBy = static (int source, int? increment) =>
{
    // Same as source + (increment.HasValue ? increment.Value : 1)
    return source + (increment ?? 1);
};

Console.WriteLine(IncrementBy(5, null)); // 6
Console.WriteLine(IncrementBy(5, 2));    // 7

With C# 12 🤓

Instead, with default lambda parameters, you can now define default values for lambda parameters directly in the lambda expression. The syntax for default lambda parameters is similar to the syntax for default parameters in methods. The default value is specified after the parameter name and an equals sign (=). Consider the following example:

var IncrementBy = static (int source, int increment = 1) =>
{    
    return source + increment;
};

Console.WriteLine(IncrementBy(10));     // 11
Console.WriteLine(IncrementBy(10, 20)); // 30

Lambda expressions follow the same rules as methods when it comes to default parameters. The default value must be a compile-time constant, and it must be of the same type as the parameter. The default value is evaluated at compile time, and the parameter is optional when calling the lambda expression.

delegate int (int arg1, int arg2 = 1);

This means that when you could technically call the lambda expression with the name of the parameter but it has to be the name generated by the anonymous function. For example, consider the following extended example:

var IncrementByWithOffset = static (int source, int increment = 1, int offset = 100) =>
{    
    return source + increment + offset;
};

Console.WriteLine(IncrementByWithOffset(10));             // 111
Console.WriteLine(IncrementByWithOffset(10, 20));         // 130
Console.WriteLine(IncrementByWithOffset(10, 20, 0));      // 30
Console.WriteLine(IncrementByWithOffset(10, arg2: -100)); // 10
Console.WriteLine(IncrementByWithOffset(10, arg3: 0));    // 11

ASP.NET Core Minimal API Example 🌐

Let’s consider an example where we have an ASP.NET Core Minimal API that uses default lambda parameters. Using the File > New > Project dialog in Visual Studio 2022, create a new ASP.NET Core Web API project. Alternatively, you can use the following .NET CLI command to create a new project:

dotnet new webapi -n WebApi

This template creates a new ASP.NET Core Web API project with a single /weatherforecast endpoint. The /weatherforecast endpoint returns an array of five random weather forecasts, consider the following template code from the Program.cs file:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

There’s a bit of code here from the template, and it’s not really the focus of our concern. Let’s focus on only the MapGet functionality, as it maps our lambda functionality to an HTTP GET call.

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

The /weatherforecast endpoint returns an array of five weather forecasts. The hardcoded five in the Enumerable.Range(1, 5) method call can be replaced with a default lambda parameter, consider the following updated code snippet:

app.MapGet("/weatherforecast", (int days = 5) =>
{
    // Safety check to ensure the days parameter is 
    // at least 1, but no more than 50.
    var count = days is > 0 and <= 50 
        ? days
        : 5;

    var forecast = Enumerable.Range(1, count).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();

    return forecast;
})

With this modified code, the MapGet method now accepts an optional days parameter with a default value of 5. So while the same default behavior exists, we’re exposing the parameter to consumers. The days parameter can be passed to the API as a query string. For example, consider the following HTTP request that asks for a 21-day weather forecast:

GET /weatherforecast?days=21 HTTP/1.1
Host: localhost:7240
Scheme: https

This default value is used when the days parameter is not provided from the query string. The days parameter is used to specify the number of days for which the weather forecast should be generated. For more information on the ASP.NET Core Minimal APIs, see optional parameters.

Next steps 🚀

That’s a wrap for this four-part series on C# 12 features! I hoped you enjoyed learning about these new features and how they can help you refactor your code.

In this post you learned about the default lambda parameters feature in C# 12. This feature allows developers to express default parameter values in lambdas. Be sure to try this out in your own code! For additional resources, I encourage you to check out the following links:

Category
.NETC#

Author

David Pine
Senior Content Developer

David Pine works in Developer Relations at Microsoft, focusing on .NET and Azure developer content. He is recognized as a Google Developer Expert in Web Technologies, and is Twilio Champion. David focuses on the developer community, actively seeking opportunities to share knowledge through speaking engagements around the world. David advocates for open-source, the .NET Foundation, C#, TypeScript, SignalR, Reactive Extensions, Azure and .NET. He's a founding member and co-host of The .NET Docs ...

More about author

0 comments

Discussion are closed.