Coming in ASP.NET Core 2.1 - top-level MVC parameter validation

This post looks at a feature coming in ASP.NET Core 2.1 related to Model Binding in ASP.NET Core MVC/Web API Controllers. I say it's a feature, but from my point of view it feels more like a bug-fix!

Note, ASP.NET Core 2.1 isn't actually in preview yet, so this post might not be accurate! I'm making a few assumptions from looking at the code and issues, I haven't tried it out myself yet.

Model validation in ASP.NET Core 2.0

Model validation is an important part of the MVC pipeline in ASP.NET Core. There are many ways you can hook into the validation layer (using FluentValidation for example), but probably the most common approach is to decorate your binding models with validation attributes from the System.ComponentModel.DataAnnotations namespace. For example:

public class UserModel  
{
    [Required, EmailAddress]
    public string Email { get; set; }

    [Required, StringLength(1000)]
    public string Name { get; set; }
}

If you use the UserModel in a controller's action method, the MvcMiddleware will automatically create a new instance of the object, bind the properties of the model, and validate it using three sources:

  1. Form values – Sent in the body of an HTTP request when a form is sent to the server using a POST
  2. Route values – Obtained from URL segments or through default values after matching a route
  3. Querystring values – Passed at the end of the URL, not used during routing.

Note, currently, data sent as JSON isn't bound by default. If you wish to bind JSON data in the body, you need to decorate your model with the [FromBody] attribute as described here.

In your controller action, you can simply check the ModelState property, and find out if the data provided was valid:

public class CheckoutController : Controller  
{
    public IActionResult SaveUser(UserModel model)
    {
        if(!ModelState.IsValid)
        {
            // Something wasn't valid on the model
            return View(model);
        }

        // The model passed validation, do something with it
    }
}

This is all pretty standard MVC stuff, but what if you don't want to create a whole binding model, but you still want to validate the incoming data?

Top-level parameters in ASP.NET Core 2.0

The DataAnnotation attributes used by the default MVC validation system don't have to be applied to the properties of a class, they can also be applied to parameters. That might lead you to think that you could completely replace the UserModel in the above example with the following:

public class CheckoutController : Controller  
{
    public IActionResult SaveUser(
        [Required, EmailAddress] string Email 
        [Required, StringLength(1000)] string Name)
    {
        if(!ModelState.IsValid)
        {
            // Something wasn't valid on the model
            return View(model);
        }

        // The model passed validation, do something with it
    }
}

Unfortunately, this doesn't work! While the properties are bound, the validation attributes are ignored, and ModelState.IsValid is always true!

Top level parameters in ASP.NET Core 2.1

Luckily, the ASP.NET Core team were aware of the issue, and a fix has been merged as part of ASP.NET Core 2.1. As a consequence, the code in the previous section behaves as you'd expect, with the parameters validated, and the ModelState.IsValid updated accordingly.

As part of this work you will now also be able to use the [BindRequired] attribute on parameters. This attribute is important when you're binding non-nullable value types, as using the [Required] attribute with these doesn't give the expected behaviour.

That means you can now do the following for example, and be sure that the testId parameter was bound correctly from the route parameters, and the qty parameter was bound from the querystring. Before ASP.NET Core 2.1 this won't even compile!

[HttpGet("test/{testId}")]
public IActionResult Get([BindRequired, FromRoute] Guid testId, [BindRequired, FromQuery] int qty)  
{
    if(!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    // Valid and bound
}

For an excellent description of this problem and the difference between Required and BindRequired, see this article by Filip.

Summary

In ASP.NET Core 2.0 and below, validation attributes applied to top-level parameters are ignored, and the ModelState is not updated. Only validation parameters on complex model types are considered.

In ASP.NET Core 2.1 validation attributes will now be respected on top-level parameters. What's more, you'll be able to apply the [BindReqired] attribute to parameters.

Add comment