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

Conditional ASP.NET MVC Validation using Data Annotation Attributes

0.00/5 (No votes)
12 Aug 2014 1  
Declaration of conditional logic for Data annotation validation attributes

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

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