Simplifying Model Validation in ASP.NET Core Projects

Background

When developing an ASP.NET Core Web Application or Web API project, one task we perform quite often is model/object state validation. We like to make sure that what we are receiving is what we are expecting, right? Of course! That’s what writing good code is about! If we are going to extend functionality to someone or something to use, we should always ensure that the way in which our functionality is used, is a way in which we expect it to be used.

The downside here, however, is that validating our object state is plagued by one of the most annoying aspects to writing/reading code… verbose code that essentially does the same thing, but is hard to abstract out to a method.

The Problem

So we have the background, but what does this mean? Well, suppose we have the following code:

[HttpPost]
public IActionResult SomePostMethod([FromBody] SomeModel someModel)
{
    //Do something with someModel

    return Ok();
}

where SomeModel is defined as:

public class SomeModel
{
    // Must not be null or whitespace
    public string SomeString { get; set; }

    // Must be greater than 0
    public int SomeInt { get; set; }
}

We want to be good developers, so we want to make sure that someModel is valid (i.e. that it is not null and that all the members in it are not null and have a value). So we do it as follows:

[HttpPost]
public IActionResult SomePostMethod([FromBody] SomeModel someModel)
{
    if(someModel == null)
    {
        return BadRequest("{nameof(someModel)} was null");
    }

    if (string.IsNullOrEmpty(someModel.SomeString))
    {
        return BadRequest("{nameof(someModel.SomeString)} was null or empty");
    }

    if(someModel.SomeInt <= 0)
    {
        return BadRequest($"{nameof(someModel.SomeInt)} was less than or equal to 0");
    }

    //Do something with someModel

    return Ok();
}

Okay, cool. We’ve validated our object, but look how long and verbose that validation checking is! It’s easy to think something like “But… what if you put all the if checks into one if statement using OR logic?”. It is correct that this reduces lines of code, but it does not reduce the amount of typing and checking involved (plus, you lose your ability to have very specific error messages).

So, now we have validation, but we don’t like it because it’s too verbose. Then it hits us! We remember that .NET has some built in validation for controller methods which uses the ModelStateDictionary and we can use Data Annotations to make validation easy! So, we rewrite SomeModel as follows:

public class SomeModel
{
    [Required]
    public string SomeString { get; set; }

    [Range(1, int.MaxValue)]
    public int SomeInt { get; set; }
}

and we rewrite our code as follows:

[HttpPost]
public IActionResult SomePostMethod([FromBody] SomeModel someModel)
{
    // Still have to check if this is null as
    // ModelState allows ActionArguments to be null
    if(someModel == null)
    {
        return BadRequest($"{nameof(someModel)} was null");
    }

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

    //Do something with someModel

    return Ok();
}

Awesome! This is a lot cleaner and way more simple. The downside here though, is that we always have to check if the model being passed in is null or not because the model state validation allows action arguments to be null. Plus, every controller method is going to have at least two similar if checks which is going to cause your controller methods to look pretty cluttered.

The Solution

So what do we do then? It’s as simple as it can get, right? Wrong! What if I told you that we can perform the above validation without writing a single if check in our controller methods? Would you believe me? Well, guess what… we can perform the above validation without writing a single if check in our controller methods! Even better, it’s extremely simple to do.

First, in order to do this, we are going to be using an ActionFilterAttribute. If you are unsure of what Attributes or Filters are, you should check out this article. We begin by creating a class that inherits from the ActionFilterAttribute class:

[AttributeUsage(AttributeTargets.Method)]
public sealed class ValidateModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // First, check all action arguments and make sure they aren't null
        foreach (var argument in context.ActionArguments)
        {
            if (argument.Value == null)
            {
                context.ModelState.AddModelError(argument.Key, $"{argument.Key} cannot be null");
            }
        }

        // Now we check if the model stat is valid. The IsValid property
        // Checks to see if any erros have been added to the ModelState
        // Since we add an error if any action arguments are null, this
        // Would be true if we found any. It will also be true if the
        // Supplied model to the controller failed validation from data
        // Annotations.
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

We then use our new filter as follows:

[HttpPost]
[ValidateModelState]
public IActionResult SomePostMethod([FromBody] SomeModel someModel)
{
    //Do something with someModel

    return Ok();
}

Wow! That really cleaned up our controller method! The best part though, is now if we want this validation on any method, we just add our filter to the method and BAM! We have object state validation. You could even go as far as putting the filter on a class level, or even add it into your global filters on startup. I personally stray away from this because not every controller method is going to need this validation, so I add it specifically to the ones that do.

TL;DR

We can simplify our object state validation by using Data Annotations and a custom action filter. You can checkout a complete, working example on my GitHub

Checkout my GitHub: https://github.com/StephenMP

Leave a comment