How to do model validation in every method in ASP.NET Core Web API?

35,792

Solution 1

How to check the model state?

Check the controller's ModelState in the action to get the state of the model.

getting a readable string out of all errors and return a BadRequest with this error?

Use BadRequest(ModelState) to return HTTP bad request response which will inspect the model state and construct message using errors.

Completed code

/// <summary>
/// API endpoint to login a user
/// </summary>
/// <param name="data">The login data</param>
/// <returns>Unauthorizied if the login fails, The jwt token as string if the login succeded</returns>
[AllowAnonymous]
[Route("login")]
[HttpPost]
public IActionResult Login([FromBody]LoginData data) {
    if(ModelState.IsValid) {
        var token = _manager.ValidateCredentialsAndGenerateToken(data);
        if (token == null) {
            return Unauthorized();
        } else {
            return Ok(token);
        }
    }
    return BadRequest(ModelState);
}

Of course I could write it all myself in a helper method... But I thought about a filter maybe?

To avoid the repeated ModelState.IsValid code in every action where model validation is required you can create a filter to check the model state and short-circuit the request.

For example

public class ValidateModelAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext context) {
        if (!context.ModelState.IsValid) {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Can be applied to the action directly

[ValidateModel] //<-- validation
[AllowAnonymous]
[Route("login")]
[HttpPost]
public IActionResult Login([FromBody]LoginData data) {
    var token = _manager.ValidateCredentialsAndGenerateToken(data);
    if (token == null) {
        return Unauthorized();
    } else {
        return Ok(token);
    }    
}

or added globally to be applied to all request where model state should be checked.

Reference Model validation in ASP.NET Core MVC

Solution 2

I would Highly recommend using [ApiController] and other attributes that help ease validation in web API based projects.

[ApiController] this attribute does all basic validation on the modal for you before it enters the method. So you only have to inspect the modal if your want to do some form of custom validation.

Solution 3

To check if the model state is valid use the ModelState property (exposed by the ControllerBase class which the Controller class inherits from)

ModelState.IsValid

To get the errors from the ModelState you could filter out the errors from the dictionary and return them as a list

var errors = ModelState
    .Where(a => a.Value.Errors.Count > 0)
    .SelectMany(x => x.Value.Errors)
    .ToList();

One option is then to validate the state in every method/controller but i recommend you to implement the validation in a base class which validates the model in the
OnActionExecuting method like this

public class ApiController : Controller
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!ModelState.IsValid)
        {
            var errors = ModelState
                .Where(a => a.Value.Errors.Count > 0)
                .SelectMany(x => x.Value.Errors)
                .ToList();
            context.Result = new BadRequestObjectResult(errors);
        }
        base.OnActionExecuting(context);
    }
}

Then every controller which should have automatic model state validation just inherit from the base class

public class TokenController : ApiController
{
    /// <summary>
    /// API endpoint to login a user
    /// </summary>
    /// <param name="data">The login data</param>
    /// <returns>Unauthorizied if the login fails, The jwt token as string if the login succeded</returns>
    [AllowAnonymous]
    [Route("login")]
    [HttpPost]
    public IActionResult Login([FromBody]LoginData data)
    {
        var token = _manager.ValidateCredentialsAndGenerateToken(data);
        if (token == null)
        {
            return Unauthorized();
        }
        else
        {
            return Ok(token);
        }
    }
}
Share:
35,792

Related videos on Youtube

PassionateDeveloper
Author by

PassionateDeveloper

Fullstack Devleoper at www.passionate-developer.com --------------------------------------- People learn with there challanges!

Updated on January 12, 2021

Comments

  • PassionateDeveloper
    PassionateDeveloper over 3 years

    I am getting into ASP.NET Core 2.0 with Web API. One of my first methods are my login:

    /// <summary>
    /// API endpoint to login a user
    /// </summary>
    /// <param name="data">The login data</param>
    /// <returns>Unauthorizied if the login fails, The jwt token as string if the login succeded</returns>
    [AllowAnonymous]
    [Route("login")]
    [HttpPost]
    public IActionResult Login([FromBody]LoginData data)
    {
        var token = _manager.ValidateCredentialsAndGenerateToken(data);
        if (token == null)
        {
            return Unauthorized();
        }
        else
        {
            return Ok(token);
        }
    }
    

    My LoginData using DataAnnotations:

    public class LoginData
    {
        [Required]
        [MaxLength(50)]
        public string Username { get; set; }
    
        [Required]
        public string Password { get; set; }
    
        [Required]
        [MaxLength(16)]
        public string IpAddress { get; set; }
    }
    

    So my ModelState is well filled automatically when the login happens and e.g. the password is empty (of course on client side there should be a validation too for it later).

    What is the best way to

    • check the model state,
    • getting a readable string out of all errors and
    • return a BadRequest with this error?

    Of course I could write it all myself in a helper method. But I thought about a filter maybe?

    • Kirk Larkin
      Kirk Larkin almost 6 years
      You might want to consider using WebAPIContrib.Core, which includes a ValidationAttribute (filter) for this. At the very least, it could be inspiration for how to build something yourself.
  • CodeNotFound
    CodeNotFound almost 6 years
    @Kovu as you say in your post you could write a helper method. But I think filter attrribute is better wuited for this. So your action method will not get bloated with repetitive code. Just apply the filter globally to all action with POST method.
  • Nkosi
    Nkosi almost 6 years
    @CodeNotFound that is a very good suggestion. model state can apply to GET requests as well so it does not necessarily have to be limited to only POST request.
  • CodeNotFound
    CodeNotFound almost 6 years
    Indeed. ModelState can be verified with GEt requests too. But GET request URLs are most of the time generated by you the developer. So if the user modified the browser bar it will get 404 Not Found. In most of the cases, I tend to avoid MoselState validation beause most of those types of request use simple parameters (typically value types) so I use route constraints. If constraints not respected it is automatically redirected to 404.
  • Nkosi
    Nkosi almost 6 years
    @CodeNotFound I guess I should have clarified that I was referring to complex models that have the data annotations. I see your point. they are valid. :)
  • CodeNotFound
    CodeNotFound almost 6 years
    To get the errors from the ModelState you could filter out the errors from the dictionary and return them as a list In a very very rare cases someone need to do this :) Just BadRequest(ModelState); or new BadRequestObjectResult(context.ModelState);for the most cases
  • Marcus Höglund
    Marcus Höglund almost 6 years
    @CodeNotFound thanks for the info. I guess my list is a corner case where I use the SelectMany to get a flatten out array with just the errors.
  • The Thirsty Ape
    The Thirsty Ape about 4 years
    For ASP.NET Core 2.1 and above this should be the accepted answer.
  • Stuart
    Stuart almost 4 years
    you can customise the default response if you wish using InvalidModelStateResponseFactory docs.microsoft.com/en-us/aspnet/core/web-api/…
  • havij
    havij about 2 years
    @Nkosi how would you unit test [ValidateModel]?
  • Nkosi
    Nkosi about 2 years
    @havij check the answer given here stackoverflow.com/questions/46162940/…