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.
Leave a Reply