Recently, I’ve been involved in a project where an old ASP.NET Framework solution has been migrated to ASP.NET Core 8.
Today, a bug report was filed: a form that was posted to one of the endpoints could no longer be saved, and a 500 Internal Server Error was returned.
The error occurred due to a NullReferenceException
. The action method looked something like this:
[HttpPost("/")]
public ActionResult Index( [FromForm] MyForm myForm)
{
If(myForm.Input1.Length > 0)
{
// Do some work...
}
return Ok();
}
public class MyForm
{
public string Input1 { get; init; }
}
Nothing weird, right? (Ignore the non-existing error handling, it's just for the post).
The code that posted the form looked like this:
const formData = new URLSearchParams();
formData.append('Input1', '');
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString()
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
The important thing here is that Input1 is set to an empty string ''
.
Given the above, what would you expect to happen when posting this form to the ASP.NET Core action method?
- ASP.NET Core will set Input1 to an empty string – since that’s what was posted.
- ASP.NET Core will set Input1 to
null
.
There’s only one way to find out. I've updated my endpoint to simply return whatever gets posted to it:
public class MyController : ControllerBase
{
[HttpPost("/")]
public ActionResult Index([FromForm] MyForm myForm)
{
return Ok(myForm);
}
}
public class MyUnitTest : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public MyUnitTest(WebApplicationFactory<Program> factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
[Fact]
public async Task ShouldNotSetStringToNullWhenPassingInEmptyString()
{
var client = _factory.CreateClient();
var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
new("Input1", string.Empty)
});
var request = new HttpRequestMessage(HttpMethod.Post, "/")
{
Content = content
};
var response = await client.SendAsync(request);
var responseBody = await response.Content.ReadAsStreamAsync();
var responseJson = await JsonDocument.ParseAsync(responseBody);
var input1Value = responseJson.RootElement.GetProperty("input1").GetString();
input1Value.ShouldBe(string.Empty);
}
}
As one might expect, this test... F A I L S.
When posting an empty string, ASP.NET Core does not set the corresponding property to an empty string. Instead, it sets it to null
.
The fix
You can fix this in a couple of different ways.
Endpoint specific
Decorate your property with the DisplayFormat
attribute.
public class MyForm
{
[DisplayFormat(ConvertEmptyStringToNull = false)]
public string Input1 { get; init; }
}
Globally
Create a custom IDisplayMetadataProvider
that sets ConvertEmptyStringToNull
to false
for all string
properties.
Minimal poc
using Microsoft.AspNetCore.Mvc;
using WebApplication1;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.Configure<MvcOptions>(options =>
{
options.ModelMetadataDetailsProviders.Add(new EmptyStringEnabledDisplayMetadataProvider());
});
var app = builder.Build();
app.MapControllers();
app.Run();
public class EmptyStringEnabledDisplayMetadataProvider : IDisplayMetadataProvider
{
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
if (context.Key.ModelType == typeof(string))
{
context.DisplayMetadata.ConvertEmptyStringToNull = false;
}
}
}
More info regarding this can be found in this GitHub issue