Introduction
The article discusses the design of a simple module that allows declaration of conditional logic for Data annotation validation attributes
Background
One of the amazing features of ASP.NET MVC is validation using Data annotations that is applied to both the client and server side. One of the proposed benefits of using this is the DRY concept (don’t repeat yourself). In projects, there will be cases when you need to have conditional validation depending on the action method or even the data. One big limitation of the data annotation validation approach is that they don’t have any kind of conditional logic. So if you mark a field with the RequiredAttribute
it will be required for all action methods. There are different workarounds, perhaps the most common one involving the use of view models for each action. However this goes against the DRY concept.
This article will discuss the design and usage of a module that allows adding conditional logic to data annotation validation attributes.
Design
The main class is ConditionalAttribute
that itself is derived from Attribute. ConditionalAttribute
class allows definition of data annotation attributes and specifying conditions for them.
[AttributeUsage(System.AttributeTargets.Property, AllowMultiple = true)]
public class ConditionalAttribute : Attribute
{
public Type AttributeType { get; set; }
public object[] ConstructorParam { get; set; }
public bool Apply { get; set; }
public string[] Actions { get; set; }
public string[] Keys { get; set; }
public object[] Values { get; set; }
public ConditionalAttribute(string actionName, Type attributeType)
public ConditionalAttribute(string actionName, Type attributeType, object constructorParam)
public ConditionalAttribute(string actionName, Type attributeType, string key, string value)
public ConditionalAttribute(string actionName, Type attributeType, object constructorParam, string key, string value)
public ConditionalAttribute(string actionName, Type attributeType, object constructorParam, bool apply)
public ConditionalAttribute(string actionName, Type attributeType, object[] constructorParam, string key, string value, bool apply)
}
The CustomModelValidatorProvider
class inherits from DataAnnotationsModelValidatorProvider
and contains logic to iterate through the conditional attributes, verify the condition and add the data annotation validation attributes.
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
List<Attribute> newAttributes = new List<Attribute>(attributes);
var attrsCustom = GetCustomAttributes(metadata);
if (attrsCustom != null)
{
var attrs = attrsCustom.OfType<ConditionalAttribute>().ToList();
if (attrs != null)
{
foreach (var attr in attrs)
{
if (ApplyAttribute(attr, metadata, context, attributes))
{
Attribute objAttr = CreateAttribute(attr.AttributeType, attr) as Attribute;
newAttributes.Add(objAttr);
}
}
}
}
return base.GetValidators(metadata, context, newAttributes);
}
The IConditionalAttribute
interface has a method Apply
that will be called by the CustomModelValidatorProvider
class, if the controller implements the interface. The controller can than decide whether to apply the validation attribute.
The module allows calling any validation attribute as internally it uses the Type
to create an instance and sets the properties dynamically.
virtual protected object CreateAttribute(Type type, ConditionalAttribute attr)
{
object obj = Activator.CreateInstance(type,attr.ConstructorParam);
var props = type.GetProperties();
foreach (var prop in props)
{
object value = GetKeyValue(attr, prop.Name);
if (value != null)
prop.SetValue(obj, value);
}
return obj;
}
The classes have been implemented in one single file CustomModelValidatorProvider.cs
for easy integration in projects and declared under the namespace ConditionalAttributes
.
The main constructor for the ConditionalAttribute
class is:
public ConditionalAttribute(string actionName, Type attributeType, object[] constructorParam, string key, string value, bool apply)
The actionName
parameter allows specifying the name of the action method on which the attribute should be applied. If empty it applies to all actions.
The attributeType
parameter is used for defining the type of the data annotation validation attribute.
The constructorParam
is used to specify the parameters that might be required when instantiating the data annotation attribute, for example length for StringLengthAttribute
.
The key
and value
parameters specify properties for the attributes and their values, for example ErrorMessage.
The apply
parameter reverse the logic of applying the validation attribute. For example to apply the validation attributes on all action methods except of Index
, set Index in actionName
and specify apply
=false.
Example
[ConditionalAttribute("Modify",typeof(RequiredAttribute),"ErrorMessage","{0} is required field")]
public string Address { get; set; }
This conditional attribute is used to specify a validation attribute of type RequiredAttribute. The property ErrorMessage
has the value “{0} is a required field”. The method is applied to the Modify
action method.
Let’s take at another example:
[ConditionalAttribute("Index",typeof(RemoteAttribute),Apply=false,ConstructorParam = new object[] {"NameExists", "Home" })]
public string Name {get; set; }
The statement above applies a RemoteAttribute
only when the action method name is not Index
. It specifies the constructor parameters Action (
NameExists) and Controller (
Home).
If the controller implements the IConditionalAttribute
, the Apply method
will be called internally once all the conditions have been evaluated. The method returns a Boolean that will control whether the validation attribute will be applied.
Multiple conditional attributes can be applied to a property.
public class Person
{
[ConditionalAttribute(null, typeof(RequiredAttribute), "ErrorMessage", "{0} is a required field")]
[ConditionalAttribute(null, typeof(StringLengthAttribute), 10, "ErrorMessage", "{0} cannot be more than 20 characters")]
[ConditionalAttribute(null, typeof(RegularExpressionAttribute), @"[A-Za-z0-9]*", "ErrorMessage", "{0} should be alpahnumeric characters only")]
[ConditionalAttribute("Index", typeof(RemoteAttribute), ConstructorParam = new object[] { "LoginIDExists", "Home" })]
public string LoginID { get; set; }
[ConditionalAttribute("Modify", typeof(RequiredAttribute), "ErrorMessage", "{0} is a required field")]
[ConditionalAttribute("Index", typeof(RemoteAttribute), Apply = false, ConstructorParam = new object[] { "NameExists", "Home" })]
public string Name { get; set; }
[ConditionalAttribute("Modify", typeof(RequiredAttribute), "ErrorMessage", "{0} is a required field")]
public string Address { get; set; }
}
Project
Download Sample project
The attached sample project contains two action methods. The first one is Index, and in that page the data validation is applied for the login id where as for the Name and Address no validation is applied due to the conditional logic. In the second action method, Modify the Name and Address conditional validation are applied.
The code file CustomModelValidatorProvider.cs
is inside the project App_Start folder .
Usage
To start using ConditionalAttribute
in your project follow these steps:
Copy the CustomModelValidatorProvider.cs
in your project – ideally App_Start.
In the global.asax.cs
file add the CustomerModelValidatorProvider
to the validation providers. This is done in the Application_Start
method. You will need to add ConditionalAttributes
namespace as well.
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new CustomModelValidatorProvider());
Start annotating the object model with conditional attributes.
In order to have a function that’s called during conditional attribute evaluation, inherit the controller from IConditionalAttribute
interface and implement the Apply
method.
Enhancements
A couple of enhancements that would be practical.
Add support for non - validation attributes like read only, display name etc.
Make the ConditionalAttribute
easier to call - right now it requires passing object so there is less type safety. The best option would be to pass an instance of the data annotation validation attribute with the parameters, instead of the type to the ConditionalAttribute
constructor. Suggestions are welcome how to do this in a generic fashion.
History
First version 6-Feb-2013