C# 12: Default Parameters in Lambda Expressions

In the previous blog posts of this C# 12 series you learned about different C# 12 features:

In this blog post, you will learn about another feature: Default Parameters in Lamdba Expressions.

Before we look at this feature, let’s ensure that you understand how default parameters in methods work.

Default Parameters in Methods

Since C# 4.0 you can use default parameters in methods.Default parameters are also known as optional parameters. The following PrintName method has a name parameter with the default value "Thomas".

static void PrintName(string name = "Thomas")
{
    Console.WriteLine($"Hello {name}");
}

The specified default value for the parameter allows you to call the method without an explicit argument, like you see it in the code snippet below. This means the argument is fully optional. If you call the method without argument, the text Hello Thomas is written to the console. If you call it with the argument Julia, the text Hello Julia is written to the console.

PrintName(); // Writes "Hello Thomas"
PrintName("Julia"); // Writes "Hello Julia"

With C# 12, default parameters are also supported in lambdas. Let’s take a look at this.

Default Parameters in Lamdbas

In the code snippet below you can see a lamdba expression that does exactly the same thing as the method from the previous section. The lambda has a name parameter with the default value Thomas. Specifying this default value is possible with C# 12. The lamdba itself is stored in a printName variable. As you can see in the code snippet below, because of the default parameter you can call the lambda with and without an argument.

var printName = (string name = "Thomas") =>
{
    Console.WriteLine($"Hello {name}");
};

printName(); // Writes "Hello Thomas"
printName("Julia"); // Writes "Hello Julia"

Rules for Default Parameters

Like for methods, also for lambdas the rule applies that all required parameters must appear before the default parameters. This means that the lamdba of the following code snippet does not compile, because the isDeveloper parameter is a required parameter (as it has no default value), but it appears after the default parameter name.

var printName = (string name = "Thomas", bool isDeveloper) =>
{
    var devString = isDeveloper ? " (Developer)" : "";
    Console.WriteLine($"Hello {name}{devString}");
};

When you write this code in Visual Studio, you get the error shown below that tells you that optional parameters must appear after all required parameters.

This means to make your code compile, you have two options.

Option 1) You define the isDeveloper parameter first like below

var printName = (bool isDeveloper, string name = "Thomas") =>
{
    var devString = isDeveloper ? " (Developer)" : "";
    Console.WriteLine($"Hello {name}{devString}");
};

Option 2) You define also a default value for the isDeveloper parameter

var printName = (string name = "Thomas", bool isDeveloper = true) =>
{
    var devString = isDeveloper ? " (Developer)" : "";
    Console.WriteLine($"Hello {name}{devString}");
};

What About Named Arguments?

Let’s say you have created the lamdba that is shown in the following code snippet. As you can see, it has two default parameters, name and isDeveloper.

var printName = (string name = "Thomas", bool isDeveloper = true) =>
{
    var devString = isDeveloper ? " (Developer)" : "";
    Console.WriteLine($"Hello {name}{devString}");
};

Now let’s say you want to call the lambda with just an argument for the isDeveloper parameter. But for the name parameter you want to use the default value. You might try the code below, which would work for a method with default parameters.

printName(isDeveloper: true);

But with the lambda, the C# compiler tells you that the delegate does not have an isDeveloper parameter.

This is because for lambdas, the named arguments are specified by the used delegate type. Behind the scenes, the C# compiler uses the delegates Action and Func. Action for lamdbas that return void, and Func for lambdas that have a return type. In our case here, the lamdba does not return anything, and it has two parameters, so the following Action delegate from the System namespace is used:

public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

As you can see, the parameter names in the delegate are called arg1 and arg2. You can also see this in the tooltip in Visual Studio when you hover over the printName variable like in the screenshot below. As you can see there, the delegate has the parameters arg1 and arg2.

This means actually that calling the lambda like in the code snippet below with the named argument arg2 is valid and compiles fine. So, this is what you can do if you want to call your lamdba with just a value for the isDeveloper parameter.

printName(arg2: true);

Maybe we will see in the future that the C# compiler uses a kind of flow analysis that let’s you use the parameters names of the lamdba. This would make your code more readable when using named arguments.

I had actually a little discussion with the C# team about named arguments when using lambdas. You can find it in the C# language repository in this issue that is about the new C# 12 feature to support default parameters in lambdas.

If you want to make your code more readable today when using named arguments with lambdas, an option is to define the delegate type on your own like in the following code snippet. You can see the created PrintNameDel delegate at the bottom. It uses the same parameter names as the lambda. The printName variable that stores the lambda is of type PrintNameDel. This allows you to call the lambda stored in the printName variable with a named isDeveloper argument, as this is the parameter name defined by the delegate.


PrintNameDel printName = (string name = "Thomas", bool isDeveloper = true) =>
{
    var devString = isDeveloper ? " (Developer)" : "";
    Console.WriteLine($"Hello {name}{devString}");
};

printName(isDeveloper: true);

delegate void PrintNameDel(
    string name = "Thomas",
    bool isDeveloper = true);

Work with Minimal APIs

Now you’ve seen how default parameters in lambdas work. They are useful in various cases. One of these cases is when you build a REST API with ASP.NET Core Minimal API. In a Minimal API, you use lambdas to define your endpoints. And here, default values are a great addition. Let’s take a look at an example.

Let’s take this Person record as a data structure. As you can see, it has just a FirstName property.

internal record Person(string FirstName);

In a Minimal API, you can map a route like in the code snippet below to a lambda. The route in the code snippet below is /persons. The used lamdba has a startIndex and a numberOfItems parameter. This should allow to load only a subset of the data. In the lambda, a persons variable is filled with a generated array that contains the values Thomas1 to Thomas20. In the return statement, the Skip and Take methods are used to apply the passed in parameter values and to return the corresponding subset of the persons array.

app.MapGet("/persons", (int startIndex, int numberOfItems) =>
    {
        var persons = Enumerable.Range(1, 20)
                                .Select(number => new Person("Thomas" + number))
                                .ToArray();

        return persons.Skip(startIndex)
                      .Take(numberOfItems)
                      .ToArray();
    });

Now, you when you run the Minimal API, you can try to navigate to the route /persons without passing any parameters, like you see it in the code snippet below.

https://localhost:7082/persons

When you do this, you will get a BadHttpRequestException like in the screenshot below. As you can see, it says that the required parameter “int startIndex” was not provided from the query string.

This means to successfully call the API, you have to pass the two parameters with the query string like in the following code snippet.

https://localhost:7082/persons?startIndex=0&numberOfItems=5

When you do this, you get a JSON result with the five items Thomas1 to Thomas5 like in the following screenshot.

But now, wouldn’t it be great if you could use just the route /persons and then default values for the parameters would be used? That’s now possible with C# 12, because also for Minimal APIs you can use default parameters in lambda expressions. The code below defines default values for both parameters, startIndex and numberOfItems.

app.MapGet("/persons", (int startIndex = 0, int numberOfItems = 5) =>
{
    var persons = Enumerable.Range(1, 20)
                            .Select(number => new Person("Thomas" + number))
                            .ToArray();

    return persons.Skip(startIndex)
                  .Take(numberOfItems)
                  .ToArray();
});

This means that with these default parameters in place, you can navigate to the /persons route without specifying anything additional in a query string, and then the default values are used. You can see this in the screenshot below, an array with the persons Thomas1 to Thomas5 is returned.

In the following screenshot the value 5 for the startIndex parameter is defined. This means the persons Thomas6 to Thomas10 are returned.

You can also just define the numberOfItems parameter like in the screenshow below. There it is set to 10, which means that an array with the persons Thomas1 to Thomas10 is returned from the API.

Of course, you can also specify values for both parameters like in the following screenshot. There startIndex is set to 5 and numberOfItems is set to 10, which means that an array with the persons Thomas6 to Thomas15 is returned.

Summary

You learned in this blog post about default parameters in lambda expressions. With a default parameter in place, specifying an argument when calling the lambda is optional. This is a little but powerful language feature of C# 12. As you’ve seen, it makes ASP.NET Core Minimal APIs way more powerful than before. And I’m sure there are other places in your code where this feature can be helpful.

I hope you enjoyed reading this blog post.

In the next blog post, you will learn about another C# 12 feature: The Experimental attribute.

Share this post

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.