Overview
Validation is a very crucial part of Web API implementation. Regardless of what kind of Web API you are building, the bottom line is validating a request before processing it. The common things I will do in validation are null checks, string checks, and custom validation rules. Here, I'll explain about what will be the best way to validate the requests and how important it is.
Things to consider when implementing Web API
- Validate everything: Every request should be validated before processing it whether an Action method is GET, POST, PUT or DELETE.
- Don't assume about the data you’re receiving: The data should always be assumed to be bad until it’s been through some kind of validation process.
- Validation as a future-proof quality check: Validated data is more likely to be future-proof for your Web API functionality.
- Clean code is Important.
Validation techniques
The ways given below help you to validate the data for Web API.
1. Model Validation
The model in MVC is a representation of our data structure. If you validate this data initially, then everything is good for processing. Web API has Model Binding and Model Validation support. The techniques given below will be used for the validation.
- Data annotation: This is a technique, which can be applied on a model class for an ASP.NET Web API Application to validate the data and handle validation errors. It provides a pretty easy way to enable property-level validation logic within your Model layer. ASP.NET MVC 2 includes support for data annotation attributes. Microsoft published a great article on this.
- IValidatableObject interface: Data annotation enables us to do properly level validation. How about class-level validation? How to do class-level validation methods on model objects for some custom rules? The answer is the IValidatableObject interface. IValidatableObject interface to implement custom validation rules on a Product model class. ASP.NET MVC 3 included support for the IValidatableObject interface.
Implementation of the IValidatableObject interface
Step 1. Inherited IvalidatableObject interface for the Model class.
Step 2. Now, implement the Validate method to write your custom rules to validate the model.
public class Product : IValidatableObject
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public double Price { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Math.Abs(Price) < 0)
{
yield return new ValidationResult("InvalidPrice");
}
}
}
Step 3. Controller uses ModelState.IsValid to validate the model.
public IHttpActionResult Post(Product product)
{
if (ModelState.IsValid)
{
// Do something with the product (not shown).
return Ok();
}
else
{
return BadRequest();
}
}
Step 4. (Customized Validation with Action Filter)
To avoid having to check for the model state in every Put/Post action method of every controller, we can generalize it by creating a custom action filter.
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
2. Fluent Validation
It is open-source, which uses a validation library for .NET that uses a fluent interface and lambda expressions for building validation rules. This is a very popular validation tool, lightweight and it supports all kinds of custom validation rules.
Please follow the steps given below to implement fluent validation on Web API.
1. Install NuGet package
Install-Package FluentValidation.WebAPI -Version 6.4.0 or above.
2. Model Class
namespace ProductsApi.Models
{
[Validator(typeof(ProductValidator))]
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public double Price { get; set; }
}
}
3. Product Validator
All model validation rules will be defined in this validator class.
namespace ProductsApi.BusinessServices
{
public class ProductValidator : AbstractValidator<Product>
{
/// <summary>
/// Validator rules for Product
/// </summary>
public ProductValidator()
{
RuleFor(x => x.Id).GreaterThan(0).WithMessage("The Product ID must be greater than 0.");
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("The Product Name cannot be blank.")
.Length(0, 100)
.WithMessage("The Product Name cannot be more than 100 characters.");
RuleFor(x => x.Description)
.NotEmpty()
.WithMessage("The Product Description must be at least 150 characters long.");
RuleFor(x => x.Price).GreaterThan(0).WithMessage("The Product Price must be greater than 0.");
}
}
}
4. Validation action filter
An action filter consists of the logic, which runs directly before or directly after an Action method runs. You can use action filters for logging, authentication, output caching, Validations, or other tasks.
You implement an action filter as an attribute, which is inherited from the ActionFilterAttribute class. You override the OnActionExecuting method, if you want your logic to run before the Action Method. You override the OnActionExecuted method, if you want your logic to run after the Action method. After you define an action filter, you can use the attribute to mark any action methods, to which you want the filter to apply.
namespace ProductsApi
{
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
}
Configuration custom filters in web.config file are given.
namespace ProductsApi
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
// Add Custom validation filters
config.Filters.Add(new ValidateModelStateFilter());
FluentValidationModelValidatorProvider.Configure(config);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
5. Controller
The controller has GET, POST, PUT, and DELETE actions. ProductValidator will be called before executing each Action method. In this way, all validation rules will be in one place.
namespace ProductsApi.Controllers
{
/// <summary>
/// Controller class for Products
/// </summary>
public class ProductsController : ApiController
{
/// <summary>
/// Get List of all products
/// </summary>
/// <returns></returns>
/// <remarks>
/// Get List of all products
/// </remarks>
[Route("api/v1/Products")]
public IHttpActionResult Get()
{
try
{
List<Product> productList = new List<Product>
{
new Product { Id = 1, Name = "Apple S7", Description = "Description about product Apple S7", Price = 607.99 },
new Product { Id = 2, Name = "Apple S6", Description = "Description about product Apple S6", Price = 507.99 },
new Product { Id = 3, Name = "Apple S5", Description = "Description about product Apple S5", Price = 407.99 }
};
return Ok(productList);
}
catch
{
return InternalServerError();
}
}
/// <summary>
/// Get a product
/// </summary>
/// <returns></returns>
/// <remarks>
/// Get a product by given product id
/// </remarks>
[Route("api/v1/Products/{productId}")]
public IHttpActionResult Get(int productId)
{
// Product Id is validated on ValidateModelStateFilter class
try
{
List<Product> productList = new List<Product>
{
new Product { Id = 1, Name = "Apple S7", Description = "Description about product Apple S7", Price = 607.99 },
new Product { Id = 2, Name = "Apple S6", Description = "Description about product Apple S6", Price = 507.99 },
new Product { Id = 3, Name = "Apple S5", Description = "Description about product Apple S5", Price = 407.99 }
};
Product product = productList.Where(p => p.Id.Equals(productId)).FirstOrDefault();
return Ok(product);
}
catch
{
return InternalServerError();
}
}
/// <summary>
/// Create a Product
/// </summary>
/// <param name="product"></param>
/// <returns></returns>
/// <remarks>
/// Create a product into Databse
/// </remarks>
[Route("api/v1/Products")]
public IHttpActionResult Post(Product product)
{
// product object is validated on ValidateModelStateFilter class
try
{
// Call Data base Repository to insert product into DB
return Ok();
}
catch
{
return InternalServerError();
}
}
/// <summary>
/// Update the existing product
/// </summary>
/// <param name="product"></param>
/// <returns></returns>
/// <remarks>
/// Update the existing product
/// </remarks>
[Route("api/v1/Products")]
public IHttpActionResult Put(Product product)
{
try
{
// Logic for implementing update the product on Databse
return Ok();
}
catch
{
return InternalServerError();
}
}
/// <summary>
/// Delete the product from Databse
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
/// <remarks>
/// Delete the product from Databse
/// </remarks>
[Route("api/v1/Products/{productId}")]
public IHttpActionResult Delete(int productId)
{
try
{
// Logic for implementing delete the product from Databse
return Ok();
}
catch
{
return InternalServerError();
}
}
}
}
6. Testing controller actions
- Post Method: Give an empty description for the product to add. Fluent validation gives you a bad result.
- Giving correct data.
- Get the products.
- Get a product by product ID.
- JSON Schema Validation: Schema validation comes into the picture when you are using dynamic or string to accept the request for the actions or the other scenarios.
JSON schema is used to validate the structure and the data types of a piece of JSON. The “additionalProperties” property is used to check whether the keys present in JSON or not.
public bool SchemaValidation()
{
string schemaJson = @"
{
'properties': {
'Name': {
'type': 'string'
},
'Surname': {
'type': 'string'
}
},
'type': 'object',
'additionalProperties': false
}";
var schema = JsonSchema.Parse(schemaJson);
string jsonData = @"
{
'Name': 'Manikanta',
'Surname': 'Pattigulla'
}";
var jsonObject = JObject.Parse(jsonData);
// Validate Schema
return jsonObject.IsValid(schema);
}
Conclusion
As I explained above, all three validations are good enough. You need to choose what will be the best way to validate your model or request, as per your requirements. Basically, I am a fan of Fluent Validation because of all types of rules, which it allows ( like regular expressions).