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?

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) Commented Feb 24, 2016 at 0:58
  • I can't get that to bind the query parameteres ?offset=10&limit=10. If you have a way, please answer the question. Commented Feb 24, 2016 at 1:05
  • It will bind fine - offset and limit need to be properties (with {get; set; }) and if you want defaults, then use a parameterless constructor Commented Feb 24, 2016 at 1:08
  • @coppro 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 link Commented Jun 28, 2017 at 15:20

3 Answers 3

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
        }
    }
}

Sign up to request clarification or add additional context in comments.

Comments

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.

2 Comments

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 link
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.
0
  if (Offset < 1)
        ModelState.AddModelError(string.Empty, "Enter number greater than 1");
    if (ModelState.IsValid)
    {
    }

1 Comment

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?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.