Photo by Viktor Forgacs on Unsplash
C# records are a great addition to the language. They are immutable, have value semantics, and are great for modeling data. But when it comes to model binding in ASP.NET Core, things are getting a bit more complicated. Let's see how we can deal with C# records in ASP.NET Core model binding.
C# records are a new feature in C# 9. They are a reference type, but they have value semantics. This means that they are immutable and can be compared by value. They are great for modeling data, and they are a great addition to the language.
Here is an example of a C# record:
public record Person(string FirstName, string LastName);
In this example, we have a Person
record with two properties: FirstName
and LastName
.
Model binding is the process of mapping data from an HTTP request to an object in your application. It is a fundamental part of building web applications, and it is used to handle form submissions, query string parameters, and other types of data.
In ASP.NET Core, model binding is done automatically by the framework. When you create a controller action that takes a parameter, the framework will try to bind data from the request to that parameter.
Here is an example of a controller action that takes a Person
parameter:
[HttpPost]
public IActionResult CreatePerson(Person person)
{
// ...
}
In this example, the framework will try to bind data from the request to a Person
object. This is done automatically by the framework, and it is a very powerful feature.
At a first glance, everything seems to be easy with C# records. But when it comes to model binding in ASP.NET Core, things are getting a bit more complicated.
Now there are two issues with C# records and model binding in ASP.NET Core:
Display
and Required
attributes.The shortest way (I know) to have a parameterless constructor while maintaining the positional constructor is this one - I want to avoid embrace a class
like style:
public record Person(
string? FirstName,
string? LastName
)
{
public Person() : this(
null,
null,
)
{
}
}
The Display
and Required
attributes are used to set display error messages and translate property names. Here is an naive approach to use them:
public record Person(
[Display(Name = "First Name")]
[Required(ErrorMessage = "The first name is required")]
string? FirstName,
[Display(Name = "Last Name")]
[Required(ErrorMessage = "The last name is required")]
string? LastName
)
{
public Person() : this(
null,
null
)
{
}
}
This won't work, because the Display
and Required
attributes are not recognized by the model binding. You need to do it that way:
public record Person(
[property:Display(Name = "First Name")]
[property:Required(ErrorMessage = "The first name is required")]
string? FirstName,
[property:Display(Name = "Last Name")]
[property:Required(ErrorMessage = "The last name is required")]
string? LastName
)
{
public Person() : this(
null,
null
)
{
}
}
The reason for this is, without the property:
prefix, the attributes are not recognized by the model binding because FirstName
and LastName
are not properties of the Person
but constructor parameters. By adding the property:
prefix, the attributes are emitted for generated properties instead and the model binding can recognize them.