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.
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)
{
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.