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

coppro picture coppro · Feb 24, 2016 · Viewed 7.6k times · Source

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?

Answer

Shyju picture Shyju · Feb 24, 2016

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.