In this article, we are going to talk about what is ModelState validation, why, and how to use it in ASP.NET Core.

To download the source code for the video, visit our Patreon page (YouTube Patron tier).

We always strive to separate concerns and decouple different application layers when designing our APIs. Since client input is the first thing that comes to our web API, we usually want to validate that input before further processing. Also, we want to separate that processing from the rest of the application. Let’s see how we can achieve that in ASP.NET Core.


VIDEO: Use ModelState Validation in .NET Core Properly to Clean Your Actions.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!


What is a ModelState C#?

When we talk about ModelState, we mean ModelState property of the ControllerBase abstract class in the Microsoft.AspNetCore.Mvc namespace.

It is of ModelStateDictionary type and it represents errors that come from two subsystems: model binding and model validation. The model binding arises errors that can occur when attempting to bind values from an HTTP request to an action method, generally data conversion errors. After the model binding comes model validation, the moment when we check the business rules of our domain.

Let’s explain that theory with one small example:

public class CreateBookInputModel
{
    public string? Title { get; set; }

    public string? Description { get; set; }

    public string? ISBN { get; set; }
}

For example, the model binding logic will check that proper value types are assigned to each property. That means we can’t bind the following JSON to our CreateBookInputModel since it is going to fail because the Title is not a number:

{
    "Title": 100,
    "Description": "Book description",
    "ISBN": "123456789"
}

On the other hand, checking that the ISBN string is a 10 or 13 digit number that conforms to a particular digit calculation could be part of the model validation logic.

ModelState Validation Setup

We are going to create our examples with Visual Studio 2022 and the ASP.NET Core Web API project template.

After creating the app, we are going to rename WeatherForecastController to BooksController and change the boilerplate code:

[ApiController]
[Route("[controller]")]
public class BooksController : ControllerBase 
{
}

Furthermore, we are going to place our CreateBookInputModel class in the Models folder and delete WeatherForecast class.

Also, let’s create one POST endpoint in our BooksController:

[HttpPost]
public IActionResult Post([FromBody] CreateBookInputModel createBookInputModel)
{
    return Ok(createBookInputModel);
}

For the sake of simplicity, our controller method is going to return the OK result with the same object it has received. We are not going to have an infrastructure layer in this article or work with 201 status codes for the POST action. The main focus is on the ModelState validation. 

Since our BookController inherits ControllerBase we are able to access ModelState property in our Post method, but we are not going to do that yet. Before going further with our example, let’s explain how validation works.

What Does ModelState Validation Validate?

Model binding and model validation occur before executing a controller action in our APIs. Moreover, the ModelState object has an IsValid property where we can check its state. This property is exposed since it should be an application’s responsibility to review ModelState and act accordingly. We are going to see later how the framework can help us with that.

Adding the Validation Rules to the Model

Until now, we have seen just one example of model binding validation. And that was the one we got with defining our properties in the model binding class.

That said, let’s add more validation rules to our CreateBookInputModel class:

public class CreateBookInputModel
{
    [Required]
    public string? Title { get; set; }

    [MaxLength(250)]
    public string? Description { get; set; }

    [Required]
    [StringLength(13, MinimumLength = 13)]
    public string? ISBN { get; set; }
}

[Required], [MaxLength(250)] and [StringLength(13, MinimumLength = 13)] are validation attributes from the System.ComponentModel.DataAnnotations namespace, and let us specify validation rules for model properties. If we can’t find any appropriate built-in attributes we can write custom validation attributes as well.

It is worth mentioning that we could also define our Title without [Required] attribute:

public string Title { get; set; }

Since non-nullable properties are treated as if they had a [Required(AllowEmptyStrings = true)] attribute, missing Title in the request will also result in a validation error. We can override this behavior in our Program class:

builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

Sometimes, it is good to separate validation logic from the binding models. We could achieve that by using the FluentValidation library. You can read more about it in our FluentValidation in ASP.NET Core article.

Getting the Validation Errors for Invalid Requests

Let’s see validation in action. We are going to run our solution and make a POST request via Postman with the request body:

{
    "Description": "Book description",
    "ISBN": "123456789"
}

After sending the request, we get validation errors with 400 Bad Request status code in the response:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-e98123452c30c41dcb42a9b6c0c61f00-87ee6b6464b24a46-00",
    "errors": {
        "ISBN": [
            "The field ISBN must be a string with a minimum length of 13 and a maximum length of 13."
        ],
        "Title": [
            "The Title field is required."
        ]
    }
}

Validation attributes let us specify the error messages we want to return for invalid input. Since we didn’t specify any for the ISBN property, we have got the one that the attribute generated for us. Let’s change the error message to a more appropriate one:

public class CreateBookInputModel
{
    [Required]
    public string? Title { get; set; }
    [MaxLength(250)]
    public string? Description { get; set; }
    [Required]
    [StringLength(13, MinimumLength = 13, ErrorMessage = "The ISBN must be a string with the exact length of 13.")]
    public string? ISBN { get; set; }
}

After resending the same request we now get the response with the changed error message:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-b80c1c2a49c3c63ab6d34dd87cc4bdb4-d10e4537a96b88ad-00",
    "errors": {
        "ISBN": [
            "The ISBN must be a string with the exact length of 13."
        ],
        "Title": [
            "The Title field is required."
        ]
    }
}

We have stated that it is an application’s responsibility to check ModelState and act accordingly. But in our code, we don’t check ModelState anywhere. Yet, we’ve got validation errors.

That’s possible because our controller has an [ApiController] attribute. One of its functionalities is to validate ModelState for us and return an automatic HTTP 400 response.

However, sometimes we don’t want to rely on automatic responses. For example, when the model is invalid, the proper status code should be 422 Unprocessable Entity.

Let’s see how we can achieve that.

Manually Invoking ModelState Validation

There are multiple ways of achieving the “manual” ModelState validation. Since we can access ModelState from our controller, the first way is to check the ModelState.IsValid property from our controller method:

[HttpPost]
public IActionResult Post([FromBody] CreateBookInputModel createBookInputModel)
{
    if (!ModelState.IsValid)
    {
        return UnprocessableEntity(ModelState);
    }

    return Ok(createBookInputModel);
}

We can do better. If our controller ends up having, e.g., five endpoints, we will have some duplicated code. That leads us to the second way of doing the validation, using action filters. Let’s implement one simple action filter:

public class ValidationFilterAttribute : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new UnprocessableEntityObjectResult(context.ModelState);
        }
    }

    public void OnActionExecuted(ActionExecutedContext context) {}
}

We can use that filter on our Post action after we register it as a scoped service in our Program class:

builder.Services.AddScoped<ValidationFilterAttribute>();

Now we are ready to remove the validation code from our method and use our validation filter as a service:

[HttpPost]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public IActionResult Post(CreateBookInputModel createBookInputModel)
{
    return Ok(createBookInputModel);
}

But if we try to run our solution and make a request, we will still get an automatic 400 error response. One idea to fix this could be to remove the [ApiController] attribute, and yes, our code will work. However, we shouldn’t do that since this attribute also brings other functionalities to our controller.

So, we are going to fix this differently by disabling the automatic validation in our Program class:

...

builder.Services.Configure<ApiBehaviorOptions>(options
    => options.SuppressModelStateInvalidFilter = true);

...

Now, if we run the same request as before via Postman:

{
    "ISBN": [
        "The ISBN must be a string with the exact length of 13."
    ],
    "Title": [
        "The Title field is required."
    ]
}

We can see that our input was validated, and we receive the 422 Unprocessable Entity status code.

Adding the Custom Validation Logic to the Controller

Apart from the IsValid property we can also access ModelState.AddModelError method from the controller. It allows us to implement custom business logic for our model. For example, we could add interdependency between the Title and Description properties:

[HttpPost]
public IActionResult Post([FromBody] CreateBookInputModel createBookInputModel)
{
    if (createBookInputModel.Title != null
        && createBookInputModel.Description != null
        && !createBookInputModel.Description.Contains(createBookInputModel.Title))
    {
        ModelState.AddModelError(nameof(createBookInputModel.Description), "Book description should contain book title!");
    }

    if (!ModelState.IsValid) 
    { 
        return UnprocessableEntity(ModelState); 
    }

    return Ok(createBookInputModel);
}

Conclusion

In this article, we have explained what is a ModelState validation. Also, we have shown different ways of doing validation in our API and how to override automatic validation provided by [ApiController] attribute.

 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!