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?

  1. ASP.NET Core will set Input1 to an empty string – since that’s what was posted.
  2. 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.

Nuke Explosion

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