Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A sample on ASP.NET MVC Model Validation using DataAnnotations and ModelValidators

0.00/5 (No votes)
3 Jul 2011 2  
This article gives information to the reader on how flexible ASP.NET MVC validation framework is for complex validations on the form, also explains little bit on how ModelMetadata is created internally using provider model.

Introduction

ASP.NET MVC has got a very good validation framework. Recently I was playing around with ASP.NET MVC validation stuff using data annotations and have some findings. I thought sharing the information would benefit someone who is working on the MVC validation framework.

Background and Overview

I was exploring ASP.NET MVC validation framework recently, validations can be of many types from simple to complex, for example, a required field validation, email validation, phonenumber validation, or a field depending on another field, for example, email and confirm email, password and confirm password, etc., and many other complicated validations.

First, how do we enforce the validation rules on Model, and how does the framework ensure whether model is complying to those rules. In ASP.NET, foundation for the validation framework is model metadata. Metadata of a model describes how a model should be validated at the time of model binding.

Model properties can be decorated with data Annotations part of System.Componentmodel.DataAnnotations namespace, Annotations define the model metadata, they define the validations that should work on Model when the model binds to the form data, of course we can use the same metadata to create client side validations rules, Jquery already has validation plugin.

ASP.NET Model metadata system stores metadata using many instances of ModelMetadata, below is the structure of ModelMetadata. Two important properties to notice here are containerType and Properties. ModelMetadata represents the entire Model, and each property in the model. Model metadata is represented by multiple instances of ModelMetadata objects. For Root model object Metadata containerType will be null, for properties in the model, containerType will be root model object.

public class ModelMetadata
    {       
	public virtual Dictionary<string,> AdditionalValues { get; }
	public Type ContainerType { get; }
	public virtual bool IsRequired { get; set; }
	public object Model { get; set; }
	public Type ModelType { get; }
	public virtual IEnumerable<modelmetadata> Properties { get; }
		.
		.
		.
		.		
}

ASP.NET MVC has got a lot of extension points, it gives controls for us to extend and plugin our own custom validation. It uses provider mechanism in most of the places for easy plug and play of custom functionality. We can easily replace the existing provider with our custom provider.

Once the model is decorated with annotations, ModelMetadataProvider helps in getting the Metadata associated with the model, framework uses ModelMetadataProviders.Current to get the ModelMetadataProvier (which provides ModalMetaData class built from DataAnnotations applied) for the model, we can always have our own provider if we want to customize the way ASP.NET MVC provides the metadata.

If we want to customize the default model metadata provider, extend the custom Model Metadata Provider from DataAnnotationsModelMetadataProvider and set the ModelMetadataProviders.Current property to CustomModelMetadataProvider. By default, ModelMetadataProvider is DataAnnotationsModelMetadataProvider.

ModelMetadata returned by the provider is validated by ModelValidator. For each Annotation like RequiredAttribute, RangeAttribute, there is an associated validator like RequiredAdapter, RangeAdapter, etc.

Using the Code

Can we create our own custom validator? Consider validation ConfirmPassword should match Password, or ConfirmEmail should match Email, these validations depend on two property values of Model. For instance, if we try to validate this through Attributes IsValid(object) method instead of ModelValidator, that won't work because we don't have model context available, and we need model context to compare two properties of the model.  

Here I have created an annotation called SameAsAttribute, and created associated validator SameAsValidator. SameAsAttribute provides the metadata to the Model, and SameAsValidator validates the Model based on the metadata.

The above code contains an Employee model and associated view page, controller. 

Below is how Employee view page looks like when the validation fails.

EmployeeView.JPG

Below is the Employee Model code.

public class EmployeeModel
    {
        [Required]
        [DisplayName("Employee Id")]
        public int Id { get; set; }

        [Required]
        [DataType(DataType.Text)]
        [DisplayName("Name")]
        public string Name { get; set; }

        [Required]
        [DataType(DataType.Text)]
        [DisplayName("Address")]
        public string Address { get; set; }

        [Required]
        [DataType(DataType.PhoneNumber)]
        [DisplayName("Phone Number")]
        public int phoneNumber { get; set; }

        [Required]
        [DataType(DataType.EmailAddress)]
        [DisplayName("Email")]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.EmailAddress)]
        [SameAs("Email",ErrorMessage="It should be similar to Email")]
        [DisplayName("Confirm Email")]
        public string ConfirmEmail { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [DisplayName("Password")]
        public string Password { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [DisplayName("Confirm Password")]
        [SameAs("Password", ErrorMessage = "It should be similar to Password")]
        public string ConfirmPassword { get; set; }

    }

EmployeeModel metadata is represented by many instances of ModelMetadata classes. ModelMetadata is a recursive data structure. EmployeeModel is represented by Root ModelMetadata object, and containerType property for this is null, Properties property will have a collection of ModelMetadata objects where each object pointing to a property ModelMetadata (property metadata) for e.g., Id, Name, etc. For Property metadata containerType will EmployeeModel object.

In the above model, Confirm Email, Confirm Password properties are applied with annotation SameAs, and we also set the dependant property name to which the applied property should match to. Below is the code for SameAs attribute.

public class SameAsAttribute: ValidationAttribute 
    {
        public string Property { get; set; }
        public SameAsAttribute(string Property)
        {
            this.Property = Property;

        }
        public override bool IsValid(object value)
        {
            //Any additional validation logic specific to the property can go here.
            return true;
        }       
    }

Above code takes Property as argument, which is the dependant Property (Email) to which we need to match the current property (Confirm Email). Below is the code for Validator.

public class SameAsValidator :DataAnnotationsModelValidator
    {
        public SameAsValidator(ModelMetadata metadata, 
	ControllerContext context, ValidationAttribute attribute):base
		(metadata,context,attribute)
        {

        }   
       
        public override IEnumerable<modelvalidationresult> Validate(object container)
        {
           var dependentField= Metadata.ContainerType.GetProperty
			(((SameAsAttribute)Attribute).Property);
             var field= Metadata.ContainerType.GetProperty(this.Metadata.PropertyName);
           if (dependentField != null && field !=null)
           {
               object dependentValue = dependentField.GetValue(container, null);
               object value = field.GetValue(container, null);
               if ( (dependentValue != null && dependentValue.Equals(value)))
               {
                   if (!Attribute.IsValid(this.Metadata.Model))
                   {
                       yield return new ModelValidationResult 
				{ Message = ErrorMessage };
                   }
               }
               else

               yield return new ModelValidationResult { Message = ErrorMessage };
           }
        }
    }

In the above code, SameAsValidator is extending from DataAnnotationsModelValidator, and overrides Validate method which returns enumeration of ModelValidationResult which will be appended to the ModelState errors collection by the framework.

If we don't use model validator for validation, and we override the SameAsAttribute class IsValid() method, and try to apply the validation logic in that function, we can't do so because we will not have the context of the entire model, we will only get the value of property on which the attribute is applied to. But here we need the entire model context to compare between two properties of the model.

In SameAsValidator class, Validate method gets model object (container) as parameter to the method when it is called on properties, as property object metadata container is the root model object. We get values of both dependent and target properties, compare them and prepare enumerated list of ModelValidationResult objects, framework uses this result for displaying validation summary.

In the given sample, we register the Data Annotation SameAsAttribute to SameAsValidator with the validation provider class in Global.asax.cs file.

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(SameAsAttribute), 
	typeof(SameAsValidator)); 

Points of Interest

Having a separate validator for each annotation and having the ability to supply the entire model as parameter to the validator gives us the flexibility to perform any kind of validation.

Disclaimer

This article gives the reader an idea on how to proceed with ASP.NET MVC model validation. The sample provided in this article cannot be used for direct production deployment.  

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here