Flexible Conditional Validation with ASP.NET MVC 3
What?
UPDATE: I've now blogged the follow-up post on adding client-side support
- Part 1 - Introducing RequiredIf (this post)
- Part 2 - 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:
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!
Comments
Anonymous
February 28, 2012
Just used this in my project, works well. Thanks for the post. I'd love to see it extended to support client-side validation.Anonymous
February 28, 2012
@Simon - the client-side post is in the works... (just need some time to finish writing it up!)Anonymous
July 03, 2012
It works fine. But if it is client side then it is most suitable.Anonymous
September 17, 2012
The comment has been removedAnonymous
September 17, 2012
@Stacey - thanks for that link, it's a very nice write-up of the steps to create a custom validator (including client-side support)! Although it also creates a RequiredIfAttribute validator, it is a bit different from the one in this post. The one in the article allows you to indicate that a field is required based on the value of another property, and the comparison options are equal/not-equal. The validator that I've built up in this post gives a lot more flexibility as it takes an expression for the condition. One of the examples I gave was that the Comments field is required if IsComplete is true, or if the DueOn date has passed: [RequiredIf("IsComplete || DueOn < DateTime.Now", ErrorMessage = "Must add comments if the item is complete or overdue")] The follow-up post shows how to add client-side support: blogs.msdn.com/.../flexible-conditional-validation-with-asp-net-mvc-adding-client-side-support.aspx
- Stuart
Anonymous
February 10, 2013
Hi Stuart, thank you for this blog post. It works great with simple rules. I was wondering if it would be possible to make it work with an IList object: public IList<PossibleAnswer> Value { get; set; } [RequiredIf("Value.SelectedItem()== 0", ErrorMessage = "You need to type the other option")] public string OtherOption { get; set; } Here Value.SelectedItem() does not work, but maybe there is a way ? Thanks in advance !Anonymous
March 22, 2013
Hi Stuart, I am trying to check for radiobutton instead of checkbox so I have public bool? DoYouAgree { get; set; } but I run your code if gives me error at LambdaExpression lambdaExpression = System.Linq.Dynamic.DynamicExpression.ParseLambda( objectType, typeof(bool), expression); return lambdaExpression; at create expression function and error is : Expression of type 'Boolean' expected Exception Details: System.Linq.Dynamic.ParseException: Expression of type 'Boolean' expectedAnonymous
March 27, 2013
The comment has been removed