Creating a custom ModelValidatorProvider in ASP.NET MVC
Beginning
The validation framework of ASP.NET MVC is designed in such a way that it could be easily customizable/extensible at many points. The validation system is built using lot of classes and it's quite difficult to understand all of them. Sadly, there is no much documentation available in MSDN to help out. As default, the validations are performed by decorating models and properties with validation attributes. These validation attributes are available in a separate assembly called System.ComponentModel.DataAnnotations.
Sometimes, in applications we need to apply or perform validations in different ways. For ex. say we want to store the validation rules for a model in database or in xml files. In these cases, we have to go for implementing custom validation solutions and for that understanding the built-in validation model is trivial.
In this article, we are going to see how we can create a custom ModelValidatorProvider that validates models based upon the rules specified in the xml files and works side-by-side with the other built-in validator providers.
Peeking into source code
We've to peek into source code to understand the core classes in the validation system and how they works. There are lot of classes exists in the system and explaining all of them is simply not possible through a single post.
Let me summarize some of the points regarding the built-in validation system and that could help you to follow while implementing the custom ModelValidatorProvider.
1. A class that validates an object should implement the abstract class ModelValidator. This class contains a single abstract method called Validate which takes an object as input parameter and returns a collection of ModelValidationResult as output. ASP.NET MVC comes with a built-in ModelValidator called DataAnnotationsModelValidator which validates objects based on dataannotation attributes.
2. ModelValidators are provided by ModelValidatorProviders. The duty of a ModelValidatorProvider is to return a set of ModelValidators needed to validate a model or property. The ModelValidatorProvider is an abstract class which contains a single method called GetValidators that takes ModelMetadata and ControllerContext as input parameters and returns IEnumerable<ModelValidator> as output. DataAnnotationsModelValidatorProvider is a built-in validator provider.
3. We can use more than one ModelValidatorProvider in an application, that helps us to employ a mixed validation solution in an application. For ex. we can use one provider to validates models based on attributes and other one based on xml files. All the validator providers used in an application are stored and accessed through the ModelValidatorProvidersCollection property of the static class ModelValidatorProviders.
There are two places we can go for extension directly: ModelValidator and ModelValidatorProvider. In our example, we are going to use the existing DataAnnotationsModelValidators to perform the validation and only the provider is going to be different.
Custom ModelValidatorProvider
Before implementing our custom ModelValidatorProvider let see an example of what I'm talking about. Let say we have a simple model called Event that contains three properties: Name, Place and Date.
Model
public class Event { public string Name { get; set; } public string Place { get; set; } public DateTime Date { get; set; } }
Listing 1. Event model
Let say we want to apply some validations to the Event model. The Name property is required to create an event and it's length should not exceed 50 characters. The Date property should not accept past values (There is no built-in validation attribute that does the future date validation and I've created a custom one that is available in the attached source code). The plan is to create an xml file (shown in the below listing) with the same name as the model that stores all these validations.
Event.xml
<model type="Models.Event"> <validator type="Required" property="Name" message="Name is required." /> <validator type="StringLength" property="Name" arg-int="50" message="The length of Name should not exceed 50 characters." /> <validator type="Future" property="Date" message="The Date should be a future date." /> </model>
Listing 2. The xml file that contains the validations for Event model.
The xml structure is so flat. Usually we tend to group the validations based on properties but I would like to keep things simple for this example. The type attribute of each validator element maps to a validation attribute from the System.ComponentModel.DataAnnotations assembly.
The datatype of the parameters that has to be passed to a validator constructor are appended with the attribute name. If you take the second validator rule which is a StringLength validator that takes the maximum length of the string as an integer in it's constructor. To represent the argument holds an integer data the text int is appended with the attribute name and hence it is arg-int. The same rule applies for other datatypes as well (arg-datetime, arg-bool etc).
XmlModelValidatorProvider
Here is the complete source code of custom ModelValidatorProvider (XmlModelValidatorProvider). At the higher level what the code does is, read the validations from the xml file for a property or model and produce ModelValidators from them.
////// Custom ModelValidatorProvider that returns ModelValidators based on the validation rules specified in xml files. /// public class XmlModelValidatorProvider : ModelValidatorProvider { // Dictionary to temporarily store all the validation attribute types present in System.ComponentModel.DataAnnotations assembly. public readonly Dictionary<string, Type> _validatorTypes; public string XmlFolderPath = HttpContext.Current.Server.MapPath("Models//Rules"); public XmlModelValidatorProvider() { _validatorTypes = Assembly.LoadWithPartialName("System.ComponentModel.DataAnnotations").GetTypes() .Where(t => t.IsSubclassOf(typeof(ValidationAttribute))) .ToDictionary(t => t.Name, t => t); // custom ValidationAttribute that validates a date for future value. _validatorTypes.Add("FutureAttribute", typeof(FutureAttribute)); } #region Stolen from DataAnnotationsModelValidatorProvider // delegate that converts ValidationAttribute into DataAnnotationsModelValidator internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory = (metadata, context, attribute) => new DataAnnotationsModelValidator(metadata, context, attribute); internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>() { { typeof(RangeAttribute), (metadata, context, attribute) => new RangeAttributeAdapter(metadata, context, (RangeAttribute)attribute) }, { typeof(RegularExpressionAttribute), (metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute) }, { typeof(RequiredAttribute), (metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute) }, { typeof(StringLengthAttribute), (metadata, context, attribute) => new StringLengthAttributeAdapter(metadata, context, (StringLengthAttribute)attribute) }, }; #endregion public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) { var results = new List<ModelValidator>(); // whether the validation is for a property or model // (remember we can apply validation attributes to a property or model and same applies here as well) var isPropertyValidation = metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName); var rulesPath = String.Format("{0}\\{1}.xml", XmlFolderPath, isPropertyValidation ? metadata.ContainerType.Name : metadata.ModelType.Name); var rules = File.Exists(rulesPath) ? XElement.Load(rulesPath).XPathSelectElements(String.Format("./validation[@property='{0}']", isPropertyValidation ? metadata.PropertyName : metadata.ModelType.Name)).ToList() : new List<XElement>(); // Produce a validator for each validation attribute we find foreach (var rule in rules) { DataAnnotationsModelValidationFactory factory; var validatorType = _validatorTypes[String.Concat(rule.Attribute("type").Value, "Attribute")]; if (!AttributeFactories.TryGetValue(validatorType, out factory)) { factory = DefaultAttributeFactory; } var validator = (ValidationAttribute)Activator.CreateInstance(validatorType, GetValidationArgs(rule)); validator.ErrorMessage = rule.Attribute("message") != null && !String.IsNullOrEmpty(rule.Attribute("message").Value) ? rule.Attribute("message").Value : null; results.Add(factory(metadata, context, validator)); } return results; } // read the arguments passed to the validation attribute and cast it their respective type. private object[] GetValidationArgs(XElement rule) { var validatorArgs = rule.Attributes().Where(a => a.Name.ToString().StartsWith("arg")); var args = new object[validatorArgs.Count()]; var i = 0; foreach (var arg in validatorArgs) { var argName = arg.Name.ToString(); var argValue = arg.Value; if (!argName.Contains("-")) { args[i] = argValue; } else { var argType = argName.Split('-')[1]; switch (argType) { case "int": args[i] = int.Parse(argValue); break; case "datetime": args[i] = DateTime.Parse(argValue); break; case "char": args[i] = Char.Parse(argValue); break; case "double": args[i] = Double.Parse(argValue); break; case "decimal": args[i] = Decimal.Parse(argValue); break; case "bool": args[i] = Boolean.Parse(argValue); break; default: args[i] = argValue; break; } } } return args; } }
Listing 3. XmlModelValidatorProvider
Code Discussion
Instead of explaining the code/implementation through text I thought may be a cartoon would be an interesting idea!
Registering XmlValidatorProvider
To use our XmlValidatorProvider we have to register it to the ModelValidatorProvidersCollection of ModelValidatorProviders in Global.asax.cs.
ModelValidatorProviders.Providers.Add(new XmlModelValidatorProvider());
Listing 4. Registering XmlModelValidatorProvider in Global.asax.cs
End
There are lot of things we can improve in the code like caching validation rules, refactoring the xml file to group validators based on properties and other stuff. You can easily use this same approach when we need the validation rules are stored in database as well.
I hope you enjoyed this article. Your comments are always welcomed. Thanks for reading this.