Recently, while migrating our project from .NET 6 to .NET 8, my teammate Jeremy Chan uncovered an undocumented change in model binding behaviour that seems to appear since .NET 7. This change is not clearly explained in the official .NET documentation, so it can be something developers easily overlook.
To illustrate the issue, let’s begin with a simple Web API project and explore a straightforward controller method that highlights the change.
[ApiController]
public class FooController
{
[HttpGet()]
public async void Get([FromQuery] string value = "Hello")
{
Console.WriteLine($"Value is {value}");
return new JsonResult() { StatusCode = StatusCodes.Status200OK };
}
}
Then we assume that we have nullable enabled in both .NET 6 and .NET 8 projects.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Nullable>enable</Nullable>
...
</PropertyGroup>
...
</Project>
Situation in .NET 6
In .NET 6, when we call the endpoint with /foo?value=
, we shall receive the following error.
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-5bc66c755994b2bba7c9d2337c1e5bc4-e116fa61d942199b-00",
"errors": {
"value": [
"The value field is required."
]
}
}
However, if we change the method to be as follows, the error will not be there.
public async void Get([FromQuery] string? value)
{
if (value is null)
Console.WriteLine($"Value is null!!!");
else
Console.WriteLine($"Value is {value}");
return new JsonResult() { StatusCode = StatusCodes.Status200OK };
}
The log when calling the endpoint with /foo?value=
will then be “Value is null!!!”.
Hence, we can know that query string without value will be interpreted as being null. That is why there will be a validation error when value
is not nullable.
Thus, we can say that, in order to make the endpoint work in .NET 6, we need to change it to be as follows to make the value
optional. This will not mark value
as a required field.
public async void Get([FromQuery] string? value = "Hello")
Now, if we call the endpoint with /foo?value=
, we shall receive see the log “Value is Hello” printed.
Situation in .NET 8 (and .NET 7)
Then how about in .NET 8 with the same original setup, i.e. as shown below.
public async void Get([FromQuery] string value = "Hello")
In .NET 8, when we call the endpoint with /foo?value=
, we shall see the log “Value is Hello” printed.
So, what is happening here?
In .NET 7, a new Interface IParsable was introduced. Thus, starting from the .NET 7, IParsable.TryParse API is used for binding controller action parameter values.
Initial research shows that, under the hood, .NET 7 onwards, the new model binding implementation is used and it causes this to happen.
References
KOSD, or Kopi-O Siew Dai, is a type of Singapore coffee that I enjoy. It is basically a cup of coffee with a little bit of sugar. This series is meant to blog about technical knowledge that I gained while having a small cup of Kopi-O Siew Dai.