Is it possible to use Web API model validation on query parameters?
Asked Answered
D

3

5

I am currently trying to write a Web API application where one of the parameters I'd like to validate is a query parameter (that is, I wish to pass it in in the form /route?offset=0&limit=100):

[HttpGet]
public async Task<HttpResponseMessage> GetItems(
    int offset = 0,
    int limit = 100)
{
    if (!ModelState.IsValid)
    {
        // Handle error
    }

    // Handle request
}

In particular, I want to ensure that "offset" is greater than 0, since a negative number will cause the database to throw an exception.

I went straight for the logical approach of attaching a ValidationAttribute to it:

[HttpGet]
public async Task<HttpResponseMessage> GetItems(
    [Range(0, int.MaxValue)] int offset = 0,
    int limit = 100)
{
    if (!ModelState.IsValid)
    {
        // Handle error
    }

    // Handle request
}

This does not cause any errors at all.

After a lot of painful debugging into ASP.NET, it appears to me that this may be simply impossible. In particular, because the offset parameter is a method parameter rather than a field, the ModelMetadata is created using GetMetadataForType rather than GetMetadataForProperty, which means that the PropertyName will be null. In turn, this means that AssociatedValidatorProvider calls GetValidatorsForType, which uses an empty list of attributes even though the parameter had attributes on it.

I don't even see a way to write a custom ModelValidatorProvider in such a way as to get at that information, because the information that this was a function parameter seems to have been lost long ago. One way to do that might be to derive from the ModelMetadata class and use a custom ModelMetadataProvider as well but there's basically no documentation for any of this code so it would be a crapshoot that it actually works correctly, and I'd have to duplicate all of the DataAnnotationsModelValidatorProvider logic.

Can someone prove me wrong? Can someone show me how to get validation to work on a parameter, similar to how the BindAttribute works in MVC? Or is there an alternative way to bind query parameters that will allow the validation to work correctly?

Delilahdelimit answered 24/2, 2016 at 0:55 Comment(4)
You would need a model with properties int offset and int limit and add the validation attributes to the model property. Then change the method parameter to be your model - public async Task<HttpResponseMessage> GetItems(yourModel model)Stylish
I can't get that to bind the query parameteres ?offset=10&limit=10. If you have a way, please answer the question.Delilahdelimit
It will bind fine - offset and limit need to be properties (with {get; set; }) and if you want defaults, then use a parameterless constructorStylish
@Delilahdelimit I'm sure you've already solved this, but in order to get query string parameters to bind to properties of a complex type, that complex type parameter must be annotated with [FromUri]. For more information, see linkAlforja
M
5

You can create a view request model class with those 2 properties and apply your validation attributes on the properties.

public class Req
{
    [Range(1, Int32.MaxValue, ErrorMessage = "Enter number greater than 1 ")]
    public int Offset { set; get; }

    public int Limit { set; get; }
}

And in your method, use this as the parameter

public HttpResponseMessage Post(Req model)
{
    if (!ModelState.IsValid)
    {
       // to do  :return something. May be the validation errors?
        var errors = new List<string>();
        foreach (var modelStateVal in ModelState.Values.Select(d => d.Errors))
        {
            errors.AddRange(modelStateVal.Select(error => error.ErrorMessage));
        }
        return Request.CreateResponse(HttpStatusCode.OK, new { Status = "Error", 
                                                                       Errors = errors });
    }
    // Model validation passed. Use model.Offset and Model.Limit as needed
    return Request.CreateResponse(HttpStatusCode.OK);
}

When a request comes, the default model binder will map the request params(limit and offset, assuming they are part of the request) to an object of Req class and you will be able to call ModelState.IsValid method.

Macruran answered 24/2, 2016 at 1:10 Comment(2)
For anyone wondering exactly how the "default model binder will map the request params", the complex type parameter must be annotated with [FromUri]. For more information, see linkAlforja
In .net7, the endpoint method doesn't even get called when a validation error happens. I think in this case a custom validation attribute has to be written if someone would like to handle error or error messages.Evensong
D
6

For .Net 5.0 and validating query parameters:

using System.ComponentModel.DataAnnotations;

namespace XXApi.Models
{
    public class LoginModel
    {
        [Required]
        public string username { get; set; }
        public string password { get; set; }
    }
}
namespace XXApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        [HttpGet]
        public ActionResult login([FromQuery] LoginModel model)
        {
             //.Net automatically validates model from the URL string
             //and gets here after validation succeeded
        }
    }
}

Dingman answered 30/3, 2021 at 12:36 Comment(0)
M
5

You can create a view request model class with those 2 properties and apply your validation attributes on the properties.

public class Req
{
    [Range(1, Int32.MaxValue, ErrorMessage = "Enter number greater than 1 ")]
    public int Offset { set; get; }

    public int Limit { set; get; }
}

And in your method, use this as the parameter

public HttpResponseMessage Post(Req model)
{
    if (!ModelState.IsValid)
    {
       // to do  :return something. May be the validation errors?
        var errors = new List<string>();
        foreach (var modelStateVal in ModelState.Values.Select(d => d.Errors))
        {
            errors.AddRange(modelStateVal.Select(error => error.ErrorMessage));
        }
        return Request.CreateResponse(HttpStatusCode.OK, new { Status = "Error", 
                                                                       Errors = errors });
    }
    // Model validation passed. Use model.Offset and Model.Limit as needed
    return Request.CreateResponse(HttpStatusCode.OK);
}

When a request comes, the default model binder will map the request params(limit and offset, assuming they are part of the request) to an object of Req class and you will be able to call ModelState.IsValid method.

Macruran answered 24/2, 2016 at 1:10 Comment(2)
For anyone wondering exactly how the "default model binder will map the request params", the complex type parameter must be annotated with [FromUri]. For more information, see linkAlforja
In .net7, the endpoint method doesn't even get called when a validation error happens. I think in this case a custom validation attribute has to be written if someone would like to handle error or error messages.Evensong
S
0
  if (Offset < 1)
        ModelState.AddModelError(string.Empty, "Enter number greater than 1");
    if (ModelState.IsValid)
    {
    }
Statuette answered 5/5, 2020 at 13:54 Comment(1)
In question it was asked to paas the model in Get but in answer its Post. Is it possible to do similar thing in GET?Intelligible

© 2022 - 2024 — McMap. All rights reserved.