Click here to Skip to main content
Click here to Skip to main content

Creating a custom ModelValidatorProvider in ASP.NET MVC

By , 24 Sep 2012
Rate this:
Please Sign up or sign in to vote.

Beginning

The validation framework of ASP.NET MVC is designed 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 model is trivial.

In this article, we are going to see how we can create a custom ModelValidatorProvider that validates models based on the rules specified in 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 systems 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 and returns a collection of ModelValidationResult as output. ASP.NET MVC comes with a built-in implementation called DataAnnotationsModelValidator which validates objects based on dataannotation attributes.

ModelValidator and DataAnnotationsModelValidator

ModelValidator and DataAnnotationsModelValidator

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.

ModelValidatorProvider and DataAnnotationsModelValidatorProvider

ModelValidatorProvider and DataAnnotationsModelValidatorProvider

3. We can use more than one ModelValidatorProvider in an application and 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.

ModelValidatorProviders static class

ModelValidatorProviders static class

There are two points 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 so 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; }
}

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 with the same name as the model that contains all the 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>

The XML is so flat. Usually we tend to group the validators 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 an integer in it's constructor. The type 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 validations from the XML file for a property or model and produce ModelValidators for them.

/// <summary>
/// Custom ModelValidatorProvider that returns ModelValidators based on the validation rules specified in xml files.
/// </summary>
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;
	}
}

Code Discussion

I thought it's a good idea to explain the code through a simple cartoon. Hope you like it. If you have more questions post a comment. 

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());

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. I hope you can easily use this same idea when we need the validation rules are stored in database as well.

I hope you enjoyed this article. If you have any questions or suggestions don't hesitate to post a comment. Thanks for reading this.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

After2050
Software Developer Trigent Software Private Limited
India India
I'm a software developer from south tip of India. I spent most of the time in learning new technologies. I've a keen interest in client-side technologies especially JavaScript and admire it is the most beautiful language ever seen.
 
I like sharing my knowledge and written some non-popular articles. I believe in quality and standards but blames myself for lagging them.
 
I believe in small things and they makes me happy!
Follow on   Twitter

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 24 Sep 2012
Article Copyright 2012 by After2050
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid