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

3

22

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?

Fescue answered 23/5, 2018 at 6:27 Comment(1)
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.Triforium
P
33

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

Progenitor answered 23/5, 2018 at 11:11 Comment(6)
@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.Tega
@Tega 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.Progenitor
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.Tega
@Tega 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. :)Progenitor
@Progenitor how would you unit test [ValidateModel]?Halfbreed
@Halfbreed check the answer given here #46163440Progenitor
A
36

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.

Astrogeology answered 12/6, 2019 at 16:1 Comment(2)
For ASP.NET Core 2.1 and above this should be the accepted answer.Catchall
you can customise the default response if you wish using InvalidModelStateResponseFactory learn.microsoft.com/en-us/aspnet/core/web-api/…Emersonemery
P
33

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

Progenitor answered 23/5, 2018 at 11:11 Comment(6)
@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.Tega
@Tega 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.Progenitor
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.Tega
@Tega 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. :)Progenitor
@Progenitor how would you unit test [ValidateModel]?Halfbreed
@Halfbreed check the answer given here #46163440Progenitor
C
8

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);
        }
    }
}
Cognation answered 23/5, 2018 at 11:43 Comment(2)
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 casesTega
@Tega 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.Mushro

© 2022 - 2024 — McMap. All rights reserved.