Chapter 5 covered binding, the process by which the ASP.NET Web API framework maps the incoming HTTP request to the parameters of your action methods. As part of the binding process, ASP.NET Web API runs the validation rules you set against the properties of your model classes—a feature called model validation . The greatest benefit to using model validation instead of having your validation logic in other classes like controllers, helpers, or domain classes is that the validation logic is isolated in one place instead of duplicated in multiple places where it is difficult to maintain. Model validation helps you follow the Don’t Repeat Yourself (DRY) principle.
6.1 Validation Using Data Annotations
In the .NET framework, you can use data annotation attributes to declaratively specify the validation rules. In this exercise, you will use the out-of-the-box attribute classes that are part of the System.ComponentModel.DataAnnotations namespace to enforce the validity of the incoming request.
Listing 6-1. The Employee Model
using System.ComponentModel.DataAnnotations;
public class Employee
{
[Range(10000, 99999)]
public int Id { get; set; }
public string FirstName { get; set; }
[Required]
[MaxLength(20)]
public string LastName { get; set; }
[RegularExpression("[0-1][0-9]")]
public string Department { get; set; }
}
Listing 6-2. The POST Action Method
using System.Linq;
using System.Web.Http;
using RequestValidation.Models;
public class EmployeesController : ApiController
{
public void Post(Employee employee)
{
if (ModelState.IsValid)
{
// Just be happy and do nothing
}
else
{
var errors = ModelState.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Name = e.Key,
Message = e.Value.Errors.First().ErrorMessage
}).ToList();
}
}
}
Figure 6-1. Model state errors
Now, let us examine the unique case of the Required attribute being used with a value type such as int.
Listing 6-3. The Modified Employee Class
public class Employee
{
[Range(10000, 99999)]
public int Id { get; set; }
public string FirstName { get; set; }
[Required]
[MaxLength(20)]
public string LastName { get; set; }
[Required]
public int Department { get; set; }
}
Property 'Department' on type 'HelloWebApi.Models.Employee' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required.
?config.Services.GetModelValidatorProviders().ToList()
Count = 3
[0]: {System.Web.Http.Validation.Providers.DataAnnotationsModelValidatorProvider}
[1]: {System.Web.Http.Validation.Providers.DataMemberModelValidatorProvider}
[2]: {System.Web.Http.Validation.Providers. InvalidModelValidatorProvider}
Listing 6-4. Removal of InvalidModelValidatorProvider
using System.Web.Http;
using System.Web.Http.Validation;
using System.Web.Http.Validation.Providers;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Services.RemoveAll(typeof(ModelValidatorProvider),
v => v is InvalidModelValidatorProvider);
}
}
Listing 6-5. Changes to the Post Action Method
public void Post(Employee employee)
{
if (ModelState.IsValid)
{
list.Add(employee);
}
else
{
var errors = ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Name = e.Key,
Message = e.Value.Errors.First().ErrorMessage,
Exception = e.Value.Errors.First().Exception
}).ToList();
}
}
<Employee xmlns=" http://schemas.datacontract.org/2004/07/RequestValidation.Models ">
<FirstName>John</FirstName>
<Id>12345</Id>
<LastName>Human</LastName>
</Employee>
6.2 Handling Validation Errors
In this exercise, you will handle the errors raised by model validation as part of the binding process. Model validation runs the checks and sets the ModelState accordingly. It does not fail the request by sending back the errors. You must check the ModelState and take actions that are appropriate for the requirement at hand.
In the ASP.NET Web API pipeline, after the request processing part of the message handlers runs, the authorization filters are run (if you have used the Authorize attribute). Following this, model binding and the model validation occur. Then the action filters are run. More specifically, the OnActionExecuting method of each action filter is run, and then the action method of the controller runs. Hence, we can check for ModelState and send the error back in the OnActionExecuting method of an action filter. This is ideal because the model validation is finished by the time execution comes to the action filter, but the action method of the controller is yet to run. See Figure 6-2.
Figure 6-2. Action filters in the ASP.NET Web API pipeline
Listing 6-6. The ValidationErrorHandlerFilterAttribute Class
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
public class ValidationErrorHandlerFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request
.CreateErrorResponse(
HttpStatusCode.BadRequest,
actionContext.ModelState);
}
}
}
config.Filters.Add(new ValidationErrorHandlerFilterAttribute());
{
"Message":"The request is invalid.",
"ModelState":{
"employee":[
"Required property 'Department' not found in JSON. Path '', line 1, position 85."
],
"employee.LastName":[
"The field LastName must be a string or array type with a maximum length of '20'."
]
}
}
<Employee xmlns=" http://schemas.datacontract.org/2004/07/RequestValidation.Models ">
<FirstName>John</FirstName>
<Id>12345</Id>
<LastName>Humansssssssssssssssssssssssssssssssss</LastName>
</Employee>
<Error>
<Message>The request is invalid.</Message>
<ModelState>
<employee.LastName>
The field LastName must be a string or array type with a maximum length of '20'.
</employee.LastName>
</ModelState>
</Error>
You can see that ASP.NET Web API is serializing even the errors into the appropriate media type based on the results of content negotiation.
It’s also important to notice that although we did not supply a value for department in the XML request, there is no error, unlike in the JSON behavior. Recall that in an earlier exercise we removed InvalidModelValidatorProvider. XmlMediaTypeFormatter will not work with the Required attribute against value types. It will need the DataMember(IsRequired=true) attribute to be applied to create model errors correctly.
Listing 6-7. The Employee Class with a Custom Error Message
public class Employee
{
[Range(10000, 99999)]
public int Id { get; set; }
public string FirstName { get; set; }
[Required]
[MaxLength(20,
ErrorMessage="You can enter only 20 characters.
No disrespect it is only a system constraint")]
public string LastName { get; set; }
[Required]
public int Department { get; set; }
}
{
"Message":"The request is invalid.",
"ModelState":{
"employee.LastName":[
"You can enter only 20 characters. No disrespect it is only a system constraint"
]
}
}
Listing 6-8. The Employee Class with a Custom Error Message From the Resource File
using System.ComponentModel.DataAnnotations;
using Resources;
public class Employee
{
[Range(10000, 99999)]
public int Id { get; set; }
public string FirstName { get; set; }
[Required]
[MaxLength(20,
ErrorMessageResourceName = "InvalidLastNameLength",
ErrorMessageResourceType = typeof(Messages))]
public string LastName { get; set; }
[Required]
public int Department { get; set; }
}
Listing 6-9. The CultureHandler Class
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class CultureHandler : DelegatingHandler
{
private ISet<string> supportedCultures = new HashSet<string>() { "en-us", "en", "fr-fr", "fr" };
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var list = request.Headers.AcceptLanguage;
if (list != null && list.Count > 0)
{
var headerValue = list.OrderByDescending(e => e.Quality ?? 1.0D)
.Where(e => !e.Quality.HasValue ||
e.Quality.Value > 0.0D)
.FirstOrDefault(e => supportedCultures
.Contains(e.Value, StringComparer.OrdinalIgnoreCase));
// Case 1: We can support what client has asked for
if (headerValue != null)
{
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(headerValue.Value);
Thread.CurrentThread.CurrentCulture =
Thread.CurrentThread.CurrentUICulture;
}
// Case 2: Client is okay to accept anything we support except
// the ones explicitly specified as not preferred by setting q=0
if (list.Any(e => e.Value == "*" &&
(!e.Quality.HasValue || e.Quality.Value > 0.0D)))
{
var culture = supportedCultures.Where(sc =>
!list.Any(e =>
e.Value.Equals(sc, StringComparison.OrdinalIgnoreCase) &&
e.Quality.HasValue &&
e.Quality.Value == 0.0D))
.FirstOrDefault();
if (culture != null)
{
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(culture);
Thread.CurrentThread.CurrentCulture =
Thread.CurrentThread.CurrentUICulture;
}
}
}
return await base.SendAsync(request, cancellationToken);
}
}
config.MessageHandlers.Add(new CultureHandler());
{
"Message":"The request is invalid.",
"ModelState":{
"employee.LastName":[
"You can enter only 20 characters. No disrespect it is only a system constraint"
]
}
}
{
"Message":"The request is invalid.",
"ModelState":{
"employee.LastName":[
"Vous pouvez entrer que 20 caractères. Sans manquer de respect, il est seulement une contrainte de système"
]
}
}
6.3 Extending an Out-of-the-Box Validation Attribute
When an out-of-the-box validation attribute does not meet all your requirements, you can extend it with additional functionality. To illustrate this point, in this exercise, you will extend the out-of-the-box validation attribute Range to make the range validation applicable for all the members of a collection.
Listing 6-10. The Modified Employee Class with the MemberRange Attribute
public class Employee
{
[Range(10000, 99999)]
public int Id { get; set; }
public string FirstName { get; set; }
[Required]
[MaxLength(20)]
public string LastName { get; set; }
[MemberRange(0, 9)]
public List<int> Department { get; set; }
}
Listing 6-11. The MemberRangeAttribute Class
using System.Collections;
using System.ComponentModel.DataAnnotations;
using System.Linq;
public class MemberRangeAttribute : RangeAttribute
{
public MemberRangeAttribute(int minimum, int maximum) : base(minimum, maximum) { }
public override bool IsValid(object value)
{
if (value is ICollection)
{
var items = (ICollection)value;
return items.Cast<int>().All(i => IsValid(i));
}
else
return base.IsValid(value);
}
}
This request goes through without any validation failures and an HTTP status code of 204 - No Content is returned.
The response is
{"Message":"The request is invalid.","ModelState":{"employee.Department":["The field Department must be between 0 and 9."]}}
As you can see, we have extended the out-of-the-box RangeAttribute and created a new attribute that applies the range validation logic on each of the members of the collection.
6.4 Creating Your Own Validation Attribute
In this exercise, you will create your own validation attribute that enforces validation based on the value of some other property. In the examples we have seen so far, the validation is limited to the value of a property in isolation. In reality, the rules to be checked against a property can be dependent on some other property of the same model. Say, you want to implement an internal policy related to the employee contribution to 401K, which is that an employee can contribute a maximum of 75 percent of their annual income. Here the validation rule for the contribution amount depends on a static value of 75 percent and the value of the other property that stores the annual income.
Listing 6-12. The Employee Class with a Custom Validation Attribute
public class Employee
{
[Range(10000, 99999)]
public int Id { get; set; }
public string FirstName { get; set; }
[Required]
[MaxLength(20,
ErrorMessageResourceName = "InvalidLastNameLength",
ErrorMessageResourceType = typeof(Messages))]
public string LastName { get; set; }
[MemberRange(0, 9)]
public List<int> Department { get; set; }
public decimal AnnualIncome { get; set; }
[LimitChecker("AnnualIncome", 75)]
public decimal Contribution401K { get; set; }
}
Listing 6-13. The LimitCheckerAttribute Class
using System.ComponentModel.DataAnnotations;
public class LimitCheckerAttribute : ValidationAttribute
{
public LimitCheckerAttribute(string baseProperty, double percentage)
{
this.BaseProperty = baseProperty;
this.Percentage = percentage;
this.ErrorMessage = "{0} cannot exceed {1}% of {2}";
}
public string BaseProperty { get; set; }
public double Percentage { get; set; }
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name, this.Percentage, BaseProperty);
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
decimal amount = (decimal)value;
var propertyInfo = validationContext
.ObjectType
.GetProperty(this.BaseProperty);
if (propertyInfo != null)
{
decimal baseAmount = (decimal)propertyInfo.GetValue(
validationContext.ObjectInstance, null);
decimal maxLimit = baseAmount * (decimal)this.Percentage / 100;
if(amount <= maxLimit)
return ValidationResult.Success;
}
return new ValidationResult(
FormatErrorMessage(validationContext.DisplayName));
}
}
{
"Message":"The request is invalid.",
"ModelState":{
"employee.Contribution401K":[
"Contribution401K cannot exceed 75% of AnnualIncome"
]
}
}
6.5 Implementing the IValidatableObject Interface
In this exercise, you will implement the System.ComponentModel.DataAnnotations.IValidatableObject interface in the Employee class. So far in this chapter, we have been focusing on the validity of a property in isolation. Even in the case of the custom validation attribute, our focus is on a specific property, which is Contribution401K. Although we compared it to the value of another property (AnnualIncome) in the process of validating the original property, our basic objective is validating the Contribution401K property.
By implementing the IValidatableObject interface, however, you can examine the object as a whole with all the properties to determine if the object state is valid or not. Though validation attributes help you keep the validation logic in one place, implementing IValidatableObject lets you keep all the business and the validation rules of a model class in one place, which is the class itself. Using validation attributes lets you specify the rules in a declarative way, whereas implementing IValidatableObject lets you specify the rules in an imperative way.
Listing 6-14. The Employee Class Implementing IValidatableObject
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class Employee : IValidatableObject
{
private const decimal PERCENTAGE = 0.75M;
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public decimal AnnualIncome { get; set; }
public decimal Contribution401K { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(this.Id < 10000 || this.Id > 99999)
yield return new ValidationResult("ID must be in the range 10000 - 99999");
if (String.IsNullOrEmpty(this.LastName))
yield return new ValidationResult("Last Name is mandatory");
else if(this.LastName.Length > 20)
yield return new ValidationResult(
"You can enter only 20 characters. No disrespect it is only a system constraint");
if (this.Contribution401K > Decimal.Zero &&
this.Contribution401K > this.AnnualIncome * PERCENTAGE)
yield return new ValidationResult(
"You can contribute a maximum of 75% of your annual income to 401K");
}
}
{
"Message":"The request is invalid.",
"ModelState":{
"employee":[
"ID must be in the range 10000 - 99999",
"You can enter only 20 characters. No disrespect it is only a system constraint",
"You can contribute a maximum of 75% of your annual income to 401K"
]
}
}
Summary
Binding is the process by which the ASP.NET Web API framework maps the incoming HTTP request to the parameters of your action methods. As part of the binding process, ASP.NET Web API runs the validation rules you set against the properties of your model classes; this feature is called model validation. The greatest benefit of using model validation is that your validation code is all in one place, following the Don’t Repeat Yourself (DRY) principle.
In the .NET framework, you can use data annotation attributes to specify the validation rules declaratively. You can use the out-of-the-box attribute classes that are part of the System.ComponentModel.DataAnnotations namespace to enforce the validity of the incoming request with ASP.NET Web API.
Model validation runs the checks and sets the ModelState based on the result of these checks. However, model binding does not fail the request by sending back the errors. A developer must check the ModelState and take actions that are appropriate for the requirement at hand. You can check for ModelState and send the error back in the OnActionExecuting method of an action filter. This is ideal for most cases because the model validation is complete by the time execution comes to the action filter, but the action method of the controller is yet to run.
In addition to using the out-of-the-box validation attributes from the System.ComponentModel.DataAnnotations namespace, you can also subclass an existing attribute and extend its out-of-the-box functionality. Another option is to create your own custom validation attribute by deriving from the System.ComponentModel.DataAnnotations.ValidationAttribute abstract class.
Finally, by creating a validatable object by implementing the System.ComponentModel.DataAnnotations.IValidatableObject interface, you can look at the model object as a whole with all the properties to determine if the object state is valid or not. Though validation attributes help you keep the validation logic in one place, implementing IValidatableObject lets you keep all the business and the validation rules of a model class in one place, which is the model class itself. Using validation attributes lets you specify the rules in a declarative way, whereas implementing IValidatableObject lets you specify the rules in an imperative way.