Search code examples
c#asp.net-coremodel-bindingasp.net-core-2.2

Differentiating between explicit 'null' and 'not specified' in ASP.NET Core ApiController


This is my very first question after many years of lurking here, so I hope I don't break any rules.

In some of my ASP.NET Core API's POST methods, I'd like to make it possible for clients to provide only the properties they want to update in the body of their POST request.

Here's a simplified version of my code:

[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public sealed class FooController : ControllerBase
{
    public async Task<IActionResult> UpdateFooAsync(Guid fooGuid, [FromBody]UpdateFooModel model)
    {
        ... Apply updates for specified properties, checking for authorization where needed...

        return Ok();
    }
}

public sealed class UpdateFooModel
{
    [BindProperty] public int? MaxFoo { get; set; }
    [BindProperty] public int? MaxBar { get; set; }
}

public sealed class Foo
{
    public int? MaxFoo { get; set; }
    public int? MaxBar { get; set; }
}

MaxBar and MaxFoo both are nullable integer values, where the null value signifies there's no maximum.

I'm trying to make it possible to let clients send e.g. the following to this endpoint:

  • Setting MaxBar to null, and setting MaxFoo to 10

    {
        "maxBar": null,
        "maxFoo": 10
    }
    
  • Setting MaxBar to null, not touching MaxFoo

    { "maxBar": null }
    
  • Update MaxBar to 5, not touching MaxFoo

    { "maxBar": 5 }
    

In my method UpdateFooAsync, I want to update only the properties that have been specified in the request.

However, when model binding occurs, unspecified properties are set to their default values (null for nullable types).

What would be the best way to find out if a value was explicitly set to null (it should be set to null), or was just not present in the request (it should not be updated)?

I've tried checking the ModelState, but it contained no keys for the 'model', only for the Guid typed parameter.

Any other way to solve the core problem would be welcome as well, of course.

Thanks!


Solution

  • Answering my own question here, based on @russ-w 's suggestion (thanks!): By marking a bool property in each optional property's setter, we can find out if it was provided or not.

    public sealed class UpdateFooModel
    {
        private int? _maxFoo;
        private int? _maxBar;
    
        [BindProperty] 
        public int? MaxFoo
        { 
            get => _maxFoo;
            set
            {
                _maxFoo = value;
                MaxFooSet = true;
            }
        }
    
        public bool MaxFooSet { get; private set; }
    
        [BindProperty] 
        public int? MaxBar
        { 
            get => _maxBar;
            set
            {
                _maxBar = value;
                MaxBarSet = true;
            }
        }
    
        public bool MaxBarSet { get; private set; }
    }
    

    Further improvements or other solutions are still welcome of course!