Flexible Conditional Validation with ASP.NET MVC 3

What?

UPDATE: I've now blogged the follow-up post on adding client-side support

 

My colleague Simon and I have each written conditional validators with a number of customers, and Simon has blogged about it a number of times. I’ve had another idea in this space that I’ve been meaning to post for a while. Simon’s most recent post gave me the nudge I needed, so if you haven’t read Simon’s post then now would be a good time – I’ll be building on the general concept here and showing a similar idea.

The RequiredIf validator is applied to a property to indicate that it is a required field subject to some other condition. To illustrate, consider the following view model:

     public class PendingRequestModel
    {
         [HiddenInput(DisplayValue = false)]
         public int Id { get; set; }         

 [StringLength(20)]
         public string Title { get; set; }
        
         [DataType(DataType.Date)]
         public DateTime RequestedOn { get; set; }

         [DataType(DataType.Date)]
         public DateTime DueOn { get; set; }

         public bool IsComplete { get; set; }

         [DataType(DataType.MultilineText)]
          [RequiredIf("IsComplete", true, ErrorMessage = "Must add comments if the item is complete")]  
        public string Comments { get; set; }
     }

Here we can use the RequiredIf attribute to indicate that the Comments field is required, but only when the IsComplete property is set to true – the first parameter to RequiredIf is the property name (“IsComplete”) and the second is the value to match against (true). This gives a certain amount of flexibility, but what if we wanted a condition that looks at multiple properties? We could start expanding the RequiredIf validator to take a number of pairs of property name and value, but then you end up with the problem of whether these sub-conditions should be and-ed or or-ed together. More importantly it doesn’t let us test conditions that compare properties to each other.

Clearly we could create separate validation attributes as needed to implement the different combinations of conditions, but these start to grow in number quite rapidly and end up being hard to name descriptively due to do their highly specific nature!

All this got me thinking about how else this could be achieved…

Creating the flexible conditional validator

In the example above we had the following attribute applied:

            [RequiredIf("IsComplete", true, ErrorMessage = "Must add comments if the item is complete")]

In my mind, this is describing the expression “IsComplete == true”, or more succinctly “IsComplete”, so I imagine an attribute that I could use like:

         [RequiredIf("IsComplete", ErrorMessage = "Must add comments if the item is complete")]

This would then allow for more flexibility, such as:

         [RequiredIf("IsComplete || DueOn < DateTime.Now", ErrorMessage = "Must add comments if the item is complete or overdue")]

So, how can we go about implementing this? The key part is parsing the expression string into something that can be worked with. Fortunately, there is a handy package on nuget:

image

To get started, create an MVC 3 project, open the Package Manager Console and type: “Install-Package DynamicQuery”. This will pull down the DynamicQuery package from nuget.org and add a reference to the assembly. (I’m not particularly going to discuss the DynamicQuery api – for more information, check out the “Dynamic Expressions.html” file that is added to the with the nuget package)

Now, create a RequiredIfAttribute class:

     public class RequiredIfAttribute : ValidationAttribute
 {
         private readonly string _condition;
         public RequiredIfAttribute(string condition)
         {
             _condition = condition;
         }
         protected override ValidationResult IsValid(object value, ValidationContext validationContext)
         {
             Delegate conditionFunction = CreateExpression(
 validationContext.ObjectType, _condition);
             bool conditionMet = (bool) conditionFunction.DynamicInvoke(
 validationContext.ObjectInstance);
             if (conditionMet)
             {
                 if (value == null)
                 {
                     return new ValidationResult(FormatErrorMessage(null));
                 }
             }
             return null;
         }
         private Delegate CreateExpression(Type objectType, string expression)
         {
             // TODO - add caching
             LambdaExpression lambdaExpression =
                      System.Linq.Dynamic.DynamicExpression.ParseLambda( 
                                objectType, typeof (bool), expression);
             Delegate func = lambdaExpression.Compile();
             return func;
         }
     }

The IsValid method calls the CreateExpression method to generate an delegate from the condition string, which is then invoked for the model object. If the condition matches it then ensures that the property we’re validating is set (this is passed in via the value parameter).

The CreateExpression method uses the DynamicQuery api to parse the condition string into an Expression which is then compiled into a delegate. As per the comments, you might want to consider adding some caching here to avoid repeated parsing and compilation of the delegate – some might view that as a memory leak ;-)

To be completely honest, I was expecting to have to do a lot more work to enable this scenario, but the DynamicQuery package made this pretty simple! (NOTE: there is also a dynamic query library in the Visual Studio 2010 samples: Inside C:\Program Files (x86)\Microsoft Visual Studio 10.0\Samples\1033\CSharpSamples.zip look for DynamicQuery in the LinqSamples folder).

Limitations

One big limitation is that this doesn’t currently support client-side validation. The other main limitation is that there ends up being a big magic string. I’d love to be able to solve that by using lambda expressions, but unfortunately they don’t sit well with attributes.

On the plus side, this approach gives a flexible validator for adding conditional validation and removes the need to create a lot of similar validators to encapsulate the more complex conditions.

Finally, I’ve attached a sample project. Once you open it you will need to add the DynamicQuery package as described above, and then you should be good to go!

ConditionalValidation.zip