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?
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.