Alexander Zeitler

Dealing with C# records in ASP.NET Core model binding

Published on Wednesday, February 21, 2024

Photo by Viktor Forgacs on Unsplash

Recycling

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.

What are C# records?

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 in ASP.NET Core

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.

Dealing with C# records in ASP.NET Core model binding

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:

  1. The model binding needs a parameterless constructor
  2. If you want to translate property names or set display error messages, you need to use the 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.

What are your thoughts about "Dealing with C# records in ASP.NET Core model binding"?
Drop me a line - I'm looking forward to your feedback! email
Imprint | Privacy