Web API Validation in ASP.NET Core 3.0

One important quality of good API design is how it reports errors and API validation plays the key role.

In a typical backend system, there will be three main categories of errors happening: internal server errors, authentication/authorization errors and validation errors. The latter category (validation errors) are especially important from an API design point of view. This is because the feedback that your API gives to the client system matters for both end-users and client-application developers. While, in case of internal or authentication errors, we usually want to limit how much system-guts leak to the outside. Rather, the focus is on monitoring, logging those incidents and alerting ops team for example.

So, I was checking out how do you implement web API validation in ASP.NET Core 3.0.

I wish it would be easier

As an exercise, I’ve written a sample project called ValidatedInputService. The link will get you to my GitHub repo which I recently started to keep code samples in C# and ASP.NET Core.

I must say I was a little bit disappointed with how much time it took me to get this sample down. No, not disappointed with myself 🙂 I often am, but not this time. This time I blame it on the documentation.

My impression is that while documentation for building Razor Pages or MVC web applications is very thorough, the section on implementing web APIs is not. Concepts like model validation and filters are mainly covered in docs on building Web Apps and to know how to use them for a REST service, you need to read the whole damn thing [exaggeration used for emphasis].

I this post I will briefly list steps necessary to implement validation in your web API.

Our test subject

Here’s our sample controller:

[ApiController]
[Route("[controller]")]
public class MovieController : ControllerBase
{
    private readonly MovieRepository _repository;

    public MovieController(MovieRepository movieRepository)
    {
        _repository = movieRepository;
    }

    [HttpGet]
    public ApiResponse<IEnumerable<Movie>> Get(
        string phrase = null, 
        DateTime? releasedFrom = null, 
        DateTime? releasedTo = null)
    {
        return new ApiResponse<IEnumerable<Movie>>(_repository.Find(phrase, releasedFrom, releasedTo));
    }

    [HttpPost]
    public ApiResponse<object> Post([FromBody, Required] Movie movie)
    {
        _repository.Add(movie);
        
        return ApiResponse<object>.Ok();
    }
}

And the model:

public class Movie
{
    public string Title { get; set; }
    public int Year { get; set; }
    public DateTime Release { get; set; }
    public int Runtime { get; set; }
    public string Director { get; set; }
    public string StoryBy { get; set; }
    public string[] Cast { get; set; }
    public string[] Genres { get; set; }
}

Using built-in API validation attributes

ASP.NET Core provides validation attributes under System.ComponentModel.DataAnnotations namespace. A list of available attributes can be found here. Those can be used to annotate fields on the model that should be somehow validated.

The required attribute – validates the model and not request

The first and easiest thing to achieve is to require selected fields on the model. In my case I actually want all the fields to be required.

Here’s our Movie model with Required attributes:

public class Movie
{
    [Required]
    public string Title { get; set; }
    public int Year { get; set; }
    public DateTime Release { get; set; }
    public int Runtime { get; set; }
    [Required]
    public string Director { get; set; }
    [Required]
    public string StoryBy { get; set; }
    [Required]
    public string[] Cast { get; set; }
    [Required]
    public string[] Genres { get; set; }
}

Note that I did not annotate Year and Runtime. According to this piece of documentation, all the non-nullable primitives are regarded as required by the validation system. Let’s see how this works in practice.

I send this:

{
	"title":"james bond"
}

And I get this back (this is already my custom response, which I will talk about later):

{
    "status": "CLIENT_ERROR",
    "data": null,
    "error": {
        "apiErrorCode": "VALIDATION_ERROR",
        "message": "There are validation errors",
        "validationErrors": [
            {
                "field": "Cast",
                "message": "The Cast field is required."
            },
            {
                "field": "Genres",
                "message": "The Genres field is required."
            },
            {
                "field": "StoryBy",
                "message": "The StoryBy field is required."
            },
            {
                "field": "Director",
                "message": "The Director field is required."
            }
        ]
    }
}

Hmm. Neither year or release field is being complained about. Why is that? My guess is that the model binder creates the model object first and then tries to populate its fields from the input. Then it checks what fields are required and validates that they are not null. Non-nullable primitives are never null, so everything looks fine.

At first, my impression was that this behavior is a bug. I would expect the validation peek into request, see that required fields were not explicitly specified and then report a problem. Then again, this is called ‘model validation’ and not ‘request validation’. Not a bug then?

The simple and useful MinLength attribute

A second very common requirement is about the length of strings and collections. In our sample controller, we want to ensure that the phrase in Get action is at least 3 characters long. We also want that both Cast and Genres arrays have at least one item.

MinLenght to the rescue!

[HttpGet]
public ApiResponse<IEnumerable<Movie>> Get(
    [MinLength(3)]
    string phrase = null, 
    DateTime? releasedFrom = null, 
    DateTime? releasedTo = null)
{
    return new ApiResponse<IEnumerable<Movie>>(_repository.Find(phrase, releasedFrom, releasedTo));
}

And on the model fields:

...
[Required]
[MinLength(1)]
public string[] Cast { get; set; }
[Required]
[MinLength(1)]
public string[] Genres { get; set; }
...

So that this example input:

{
	"title":"james bond",
	"cast":[],
	"genres":["action"]
}

Produces such result:

...
        "validationErrors": [
            {
                "field": "Cast",
                "message": "The field Cast must be a string or array type with a minimum length of '1'."
            },
 ...

Implementing API validation involving multiple fields

Things get more complex if what passes as a valid value on one field, depends on the value of another. Out of preexisting validation attributes that do something like that we only have CompareAttribute.

For such scenarios aspnetcore suggests to either come up with your own custom attribute or to make your model classes implement IValidatableObject.

Writing custom validation attribute didn’t work for my case 🙁

Our Get action sports a pair of date parameters in a classic from-to fashion. I wanted to have a validation parameter on this method that will ensure that this pair of dates makes sense before I send request to my imaginary database.

According to documentation validation attributes can be applied to methods, and then are evaluated before the method is fired. The problem is that I could not find any statement or example on how to read parameter values passed to the given method.

I suspected it had something to do with ValidationContext object, so I tried inspecting it in the debugger. Unfortunately, the framework won’t even call an IsValid method of my validation attribute when it’s applied on method and not on individual property.

After much searching, eventually, I gave up. I’ve dropped a question on stack overflow to see if anyone else came up with an elegant way to do this. Of course, the easiest workaround is to implement this validation logic inside the action itself.

Implementing IValidatableObject

Another place where we have cross dependencies between values is our Movie object. Here the year should correspond to the year of the release.

Such cases are much simpler and well supported. It is enough to implement IValidatableObject on your model class, and the validation system will run it for you.

public class Movie : IValidatableObject
{
    [Required]
    public string Title { get; set; }
    public int Year { get; set; }
    public DateTime Release { get; set; }
    public int Runtime { get; set; }
    [Required]
    public string Director { get; set; }
    [Required]
    public string StoryBy { get; set; }
    [Required]
    public string[] Cast { get; set; }
    [Required]
    public string[] Genres { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Year != Release.Year)
        {
            yield return new ValidationResult("Year does not correspond to Release year.", new[] {nameof(Year)});
        }
    }
}

Reporting validation errors to the client

ASP.NET Core supports returning automated error responses. If your controller is annotated with [ApiController] attributed, those responses will be switched on by default.

What I like to do in my APIs is to return the same structure of response for both successes and errors. This tends to yield cleaner client implementations. Here’s an example:

public class ApiResponse<T>
{
    public static ApiResponse<object> Ok()
    {
        return new ApiResponse<object> { Status = ApiResponseStatus.SUCCESS };
    }
    
    public ApiResponse() {}

    public ApiResponse(ICollection<ApiValidationError> validationErrors)
    {
        this.Status = ApiResponseStatus.CLIENT_ERROR;
        this.Error = new ApiError(validationErrors);
    }

    public ApiResponse(T data)
    {
        this.Status = ApiResponseStatus.SUCCESS;
        this.Data = data;
    }

    public ApiResponseStatus Status { get; set; }
    public T Data { get; set; }
    public ApiError Error { get; set; }

}

public class ApiError
{
    public ApiError()
    {
        this.Message = "An unknown error has occurred";
        this.ApiErrorCode = ApiErrorCode.UNKNOWN_ERROR;
    }

    public ApiError(ApiErrorCode errorCode, string message) 
    {
        this.ApiErrorCode = errorCode;
        this.Message = message;
    }
    public ApiError(IEnumerable<ApiValidationError> validationErrors)
    {
        this.Message = "There are validation errors";
        this.ApiErrorCode = ApiErrorCode.VALIDATION_ERROR;
        this.ValidationErrors = validationErrors.ToArray();
    }

    public ApiErrorCode ApiErrorCode { get; set; }
    public string Message { get; set; }
    public ApiValidationError[] ValidationErrors { get; set; }
}

public class ApiValidationError
{
    public string Field { get; set; }
    public string Message { get; set; }
}

Disabling automatic problem detail responses

To achieve it we need to do two things. Firstly – to switch off the automated responses. We do it in ConfigureServices in Startup:

services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

Using our custom error response structure globally

Secondly, we don’t want to have to check for ModelState.IsValid in every controller method. To do that globally, we need to implement our own ActionFilter which will detect invalid model state and set the response we want to send back:

public class ValidationErrorResponseFilter : IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext context)
    {
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var validationErrors = context.ModelState.SelectMany(pair => pair.Value.Errors.Select(error => new ApiValidationError {Field = pair.Key, Message = error.ErrorMessage}));
            context.Result = new BadRequestObjectResult(new ApiResponse<object>(validationErrors.ToArray()));
        }
    }
}

And register it in web app config:

services.AddMvc().AddMvcOptions(options =>
{
    options.Filters.Add<ValidationErrorResponseFilter>();
});
services.AddScoped<ValidationErrorResponseFilter>();

And that’s about it

Although I certainly did not explore all the possibilities in my sample project, it was enough to show both the power and limitations of the validation system in ASP.NET Core.

Check out the repo for the complete solution.

Happy validating!

Leave a Reply

Your email address will not be published. Required fields are marked *