API Input Validation: Data Annotations vs Fluent Validation
When our API method receives a call, what’s the first thing we should do? Yes, we’re all thinking ‘validation’, aren’t we?
Whilst working on one of the public API endpoints here at cap hpi, I gave some consideration to validation and how to approach it. This blog post describes a couple of methods considered and some discussion of what we chose and why.
Of course, one could simply write code to test each of the input values and return a bad request response detailing any errors found. However, this didn’t feel like the neatest method and any validation rules written might not be easily tested in isolation or reused for different controller methods and their input models. And so began the search for a library or framework that could provide an alternative approach without such drawbacks.
The search was fruitful and two strong alternatives were found: Data Annotations, which are included as part of ASP.NET and involve placement of attributes on model properties; and Fluent Validation, an Apache project that provides a fluent API for creating validation rules. Let’s take a look at these two approaches in practice.
A model example
Suppose we have a simple API that allows users to query car valuations, which has the following input model:
public class ValuationRequestModel { public string Brand { get; set; } public DateTime? RegisteredDate { get; set; } public int? Mileage { get; set; } public List CarOptions { get; set; } }
Some reasonable validation rules for this model might include:
- Brand, RegisteredDate and Mileage are all required values
- Mileage must be greater than or equal to zero
- RegisteredDate must not be in the future
- CarOptions must contain between 0 and 10 members
Data Annotations
With Data Annotations, required values and value ranges are simple validations to achieve with built-in attributes (a list of available attributes can be found on MSDN). Validating date values and the list sizes requires custom attributes as they cannot be addressed via those already available.
To achieve the simpler validations, we first decorate the model with the appropriate attributes:
public class ValuationRequestModel { [Required] public string Brand { get; set; } [Required] public DateTime? RegisteredDate { get; set; } [Required] [Range(0, int.MaxValue, ErrorMessage = "Value must be greater than 0")] public int? Mileage { get; set; } public List CarOptions { get; set; } }
To have the validation called on the input model, we need to create an action filter to apply to the API controller. The action filter is defined thus:
public class ModelValidationActivationFilter : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { var modelState = actionContext.ModelState; if (!modelState.IsValid) { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, modelState); } } }
For any input model properties that carry data annotation attributes, their validity is checked. If any are not valid, the results are placed into a bad request response and returned to the caller. This action filter is placed on the API controller class, as in this simple implementation for our vehicle valuation example:
[RoutePrefix("vehicle")] [ModelValidationActivationFilter] public class ValuationController : ApiController { [HttpPost] [Route("valuation")] public IHttpActionResult GetValuation([FromBody] ValuationRequestModel model) { return Ok(); } }
Obviously, this API method doesn’t do much at present, but is sufficient to demonstrate the validation. A valid request to this method might look like the following JSON:
{ "Brand": "Toyota", "RegisteredDate": "2007-09-01T00:00:00", "Mileage": 75000, "CarOptions": [ "Rear parking sensors", "Integrated sat nav" ] }
If RegisteredDate is removed and Mileage set to -1, then a 400 Bad Request response is received with the following JSON body:
{ "message": "The request is invalid.", "modelState": { "model.RegisteredDate": [ "The RegisteredDate field is required." ], "model.Mileage": [ "Value must be greater than 0" ] } }
An advantage here is that we automatically provide the user a full report of everything wrong with the request. It’s worth noting that the RegisteredDate field of our model is a nullable DateTime type. If we made it non-nullable, then ASP would raise an error if it were not specified in the request, making our Required attribute unnecessary. However, this error would be raised prior to our own validation and therefore the user would not get a full report of all validation errors in a single response. For the remaining validation, we need to define custom attributes. The validation rule that RegisteredDate cannot be in the future is the simpler example here. We create a new attribute by extending ValidationAttribute and overriding the IsValid method:
public class DateNotInFutureAttribute : ValidationAttribute { public override bool IsValid(object value) { return ((DateTime)value).Date <= DateTime.Today; } }
This returns a simple comparison to the date at runtime. Note that IsValid takes an argument of type object; it is probably wise to also handle inputs of a different type or even nulls (such handling is omitted here for brevity). Validating the number of CarOptions is slightly more involved. The user of the attribute needs to provide some input to it, for which public properties can be defined, as in the following:
public class StringListLimitsAttribute : ValidationAttribute { public int MinLength { get; set; } public int MaxLength { get; set; } public override bool IsValid(object value) { var valueCast = value as IEnumerable; return valueCast.Count() >= MinLength && valueCast.Count() <= MaxLength; } }
It is an unfortunate limitation of C# attributes that they cannot be made generic, so here the attribute is tied to lists of strings, although reflection could be used to make this more generalised. Now the new attributes are defined, they can be applied to our model as follows:
public class ValuationRequestModel { [Required] public string Brand { get; set; } [Required] [DateNotInFuture(ErrorMessage = "Date must not be in the future")] public DateTime? RegisteredDate { get; set; } [Required] [Range(0, int.MaxValue, ErrorMessage = "Value must be greater than 0")] public int? Mileage { get; set; } [StringListLimits(MinLength = 0, MaxLength = 10, ErrorMessage = "Must list between 0 and 10 options")] public List CarOptions { get; set; } }
Note that these custom attributes are rather simple examples. For those situations where more information is needed about the other model values (e.g. for comparisons between values), an overload of the IsValid method can be overridden that also takes a ValidationContext object as a parameter. For some of our more complex validation requirements between various lists in the model, attributes that are applied at the class level can be defined.
Fluent Validation
Now let’s look at implementing the same validation using Fluent Validation. Rather than decorating model properties, a validator that extends the AbstractValidator type is defined, as in the following:
public class ValuationRequestValidator : AbstractValidator { public ValuationRequestValidator() { var requiredMessage = "Field is required"; RuleFor(x => x.Brand).NotNull().WithMessage(requiredMessage); RuleFor(x => x.RegisteredDate).NotNull().WithMessage(requiredMessage) .LessThan(DateTime.Today).WithMessage("Date must not be in the future"); RuleFor(x => x.Mileage).NotNull().WithMessage(requiredMessage) .GreaterThan(0).WithMessage("Mileage must be greater than or equal to 0"); RuleFor(x => x.CarOptions).Must(y => y != null && y.Count > 0 && y.Count < 11) .WithMessage("Must list between 0 and 10 options"); } }
And the model is linked to the validator via a single attribute at the class level:
[Validator(typeof(ValuationRequestValidator))] public class ValuationRequestModel { public string Brand { get; set; } public DateTime? RegisteredDate { get; set; } public int? Mileage { get; set; } public List CarOptions { get; set; } }
The same action filter defined in the previous section can be used to validate via Fluent Validation, although the attribute is placed on the controller method rather than the class, like so:
[RoutePrefix("vehicle")] public class ValuationController : ApiController { [HttpPost] [Route("valuation")] [ModelValidationActivationFilter] public IHttpActionResult GetValuation([FromBody] ValuationRequestModel model) { return Ok(); } }
If we run the same example as with the Data Annotations above, we get much the same message back:
{ "Message": "The request is invalid.", "ModelState": { "model.RegisteredDate": [ "Field is required" ], "model.TicketsRequired": [ " Mileage must be greater than or equal to 0" ] } }
For our example, we can achieve our validation requirements using only the built-in methods, as we can define the logic of our validations using lambda expressions. Should the need arise, custom validators can be defined, as is described on the Fluent Validation Codeplex page.
So, which is the best?
Well, that depends on your requirements. Each has advantages and disadvantages. For our project, we decided to go with Data Annotations for a couple of reasons. First, we liked that the validation is defined in the model, so you can easily see the specification of your model in place. The reusability of tested attributes was also attractive to us. Once we have unit tested an attribute, we can apply it elsewhere confidently and we can build up a suite of ready-made attributes that can be shared across development teams.
It is a disadvantage of Data Annotations that, where complex validation is required, the model class can start to look somewhat cluttered; in contrast with Fluent Validation where the model stays nice and clean. And, of course, another advantage for Fluent Validation is the fluent API, depending on your preference.
Either way, these are both great solutions that provide powerful validation without unnecessarily littering your controller methods with boilerplate code.