Validation and ASP .NET MVC
A common requirement for web applications is validation. Almost every action follows the same kind of pattern: if the data is invalid, inform the user of the errors and show them the form again. Otherwise, process the request and take them to another page. If you're using ASP .NET MVC, you will notice this kind of code all over the place:
public ActionResult SomeView()
{
if (!ModelState.IsValid)
{
return View();
}
// Do the real processing here, and
// redirect to some other view when
// you're done.
}
We all know that duplication is the number one enemy of maintainable code. If there is some way to not have to type in the "ModelState.IsValid" part for 50+% of the methods, then we save ourselves a lot of coding and maintaining. If you think about it, while the validation itself is different from method to method, how to respond to the validation is not. Ideally, we would like to write something like this:
public ActionResult SomeView()
{
// Do the real processing here, and
// redirect to some other view when
// you're done.
}
Fortunately, ASP .NET MVC makes it pretty easy to do this.
Prerequisites
- ASP .NET MVC
- C# 4.0 (for "dynamic")
Step 1: How to do validation on the parameters?
The first step is to figure out how to validate each of the parameters passed into the action. Since validation will be different for each parameter, this means that we have to call some method on some class to perform the actual validation.
public interface IValidator<T>
{
void Validate(T item);
}
However, we also need a way to tell the controller when some field on the model has an error on it so that it can tell the user. The user might also want to show when the value of some field is valid (maybe draw a green border around the field):
public interface IValidator<T>
{
void Validate(T item,
Action<IValidationError> notifyOnError,
Action<IValidationSuccess> notifyOnSuccess);
}
IValidationError and IValidationSuccess are what you would expect:
public interface IValidationResult
{
string PropertyName { get; }
T PropertyValue<T>();
}
public interface IValidationSuccess : IValidationResult
{
}
public interface IValidationError : IValidationResult
{
string ErrorMessage { get; }
}
Step 2: Does a parameter support validation?
The next step is to figure out whether or not a particular parameter supports validation. If so, then we need a way to get the validator for that parameter. This is pretty simple: we create an interface that all models which support validation will have to implement:
public interface IValidatable<T>
{
T GetValidator();
}
Step 3: Where to send the user on error?
The next challenge is to figure out where to send the user if there is an error. Since where to send the user might be different for different actions, our controllers will implement a method that will give us the error view given a context:
public interface IValidatableController
{
ViewResult ErrorViewGiven(ActionExecutingContext context);
}
Step 4: Hooking everything up
The idea is that every time an appropriate action happens, we iterate through all the parameters that are sent. If the parameters support validation, then we get the Validator for the parameter and call the Validate method on it. Once we're done with validating all Validatable parameters, we check to see if there were any errors. If there were errors, we perform the error operation. Otherwise, we continue to the actual method. ASP .NET MVC provides a way for us to hook into the execution of a controller action via the ActionFilterAttribute. The method we are interested in is the OnActionExecuting, which is called -- well -- just before the action is executed.
Steps to take:
- Check to see if the controller implements
IValidatableController. If not, we have nothing to do. - Setup the error handler, which will add errors via
ModelState.AddModelError - For each parameter in the action, find those that implement
IValidatable - Get the
Validatorfor each of these parameters to perform the validation. - If there are any validation errors, perform error action. Otherwise, do nothing.
The code:
public class ValidateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!(filterContext.Controller is IValidatableController))
{
return;
}
var validatableController = filterContext.Controller as IValidatableController;
Action<IValidationError> errorHandler = x =>
filterContext.Controller.ViewData.ModelState
.AddModelError(x.PropertyName, x.ErrorMessage);
foreach(var parameter in filterContext.ActionParameters)
{
if (!shouldValidateParameter(parameter.Value))
{
continue;
}
dynamic validatable = parameter.Value;
dynamic validator = validatable.GetValidator();
validator.Validate(validatable, errorHandler, null);
if (!filterContext.Controller.ViewData.ModelState.IsValid)
{
filterContext.Result = validatableController.ErrorViewGiven(filterContext);
}
}
}
private bool shouldValidateParameter(object parameter)
{
return parameter.GetType().GetInterfaces().Any(x =>
x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IValidatable<>));
}
}
Note the use of "dynamic" to get the validator. We need to declare dynamic variables because C# doesn't allow us to cast parameters to their generic types without knowing the actual types. Fortunately, per C# 4.0, we are able to declare variables as dynamic, which causes the code to be late-bound.