Conditional Validation in MVC
Update: If you like this, you'll like Mvc.ValidationTookit even more - check out this post!
Recently I put together samples for different types of validation for some customers, and one of those was Conditional Validation – that is “this field is required if another field is true”, and such like. But a few weeks later I saw my code fail! I got home and tried to reproduce it, and couldn’t…
So this blog post is a sorry tale of how I spent hours and hours trying to work around my own dumb mistake. Huge thanks go to Stuart who didn’t call me too many names for wasting both our time J
The Approach
Fundamentally the approach to getting conditional validation working is to use the Validator and the Attribute in tandem. Check out the docs for the DataAnnotationsModelValidator and ValidationAttribute classes if you’ve not heard of these.
The problem you initially come across with validating based on the content in another field is that the signature of the Attribute’s IsValid method looks like this;
public override bool IsValid(object value)
Notice the absence of any reference to the rest of the entity being validated, or any validation context, so you can’t check the value of another field. Now MVC actually calls Validate on your Validator class, which in turn uses the attribute’s IsValid method to perform the validation, and you’ll be pleased to see that Validate has a much more useful signature;
public override IEnumerable<ModelValidationResult> Validate(object container) { }
This actually receives the whole entity being validated. To get at extra information about it, and indeed the value currently under validation, we use the Metadata property on the validator’s base class. This means my Validate method ends up looking like this;
public override IEnumerable<ModelValidationResult> Validate(object container)
{
// get a reference to the property this validation depends upon
var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
if (field != null)
{
// get the value of the dependent property
var value = field.GetValue(container, null);
// compare the value against the target value
if ((value == null && Attribute.TargetValue == null) ||
(value != null && value.Equals(Attribute.TargetValue)))
{
// match => means we should try validating this field
if (!Attribute.IsValid(Metadata.Model))
// validation failed - return an error
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
The comments should enable you to follow the code fairly easily, but fundamentally we check the value of the Dependent Property to see whether we should perform our conditional validation. If we should, we call Attribute.IsValid.
This approach means it is essential to register your attribute and validator pair correctly in Global.asax;
DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof(RequiredIfAttribute),
typeof(RequiredIfValidator));
Easy as pie, huh? All you need to do now is create a custom attribute with DependentProperty and TargetValue properties and you’re sorted. Then you use it like this;
public class ValidationSample
{
[RequiredIf("PropertyValidationDependsOn", true)]
public string PropertyToValidate { get; set; }
public bool PropertyValidationDependsOn { get; set; }
}
Easy right?
Whoops!!!
Well, basically yes. That is all you need to do. But I made a tiny (well intentioned) mistake in my validation code that caused me to see intermittent errors. When MVC is performing Model Binding it has the following approximate flow;
- Create an instance of the model type that is required
- Loop through all properties
- Perform required field validation on the current property
- Try to set the value of a property from the value providers
- If that fails log an error in ModelState
- When all properties have been processed;
- Get a composite validator (that basically does as follows)
- Loop through all properties
- Get a list of validators
- Call validate on each one
- If validation fails log an error in ModelState
- Get a list of validators
In other words, it performs “model validation” after model binding has completed, which means you know all properties have been set accordingly before you do any validation. This means our conditional validation works great.
Unless…
Unless you inherit from RequiredAttribute for your custom attribute. I originally did this as I wanted to reuse the IsValid logic built into RequiredAttribute. But as soon as I did that, the MVC framework treated my custom conditional validation as required field validation and performed it in step 2a in the flow above, not as part of the final model validation phase. The problem with this is that other properties are not guaranteed to have been set at this point, and sure enough that was what caused me problems. As an example, imagine a class declared as follows;
public class Person
{
public string Name { get; set; }
[RequiredIf("IsUKResident", true)]
public string City { get; set; }
public bool IsUKResident { get; set; }
}
… and imagine my RequiredIfAttribute inherits from RequiredAttribute. In this scenario the validation won’t work. The logic states “validate that city is not empty if IsUKResident is true”. But the IsUKResident field is declared after the City field, so it hasn’t been model-bound when my validator is executed. The default value for a Boolean is false, so my validation that is conditional on this field being true will never get executed.
So, in summary…
The moral of the story; don’t inherit from RequiredAttribute unless this is the behaviour you want!
I’ve also attached an example application for you to play with that demonstrates working conditional validation. To see it fail as per the comments in this post, just change the implementation of RequiredIfAttribute to inherit from RequiredAttribute and delete the override of IsValid.
Comments
Anonymous
June 08, 2010
The comment has been removedAnonymous
June 08, 2010
@ Barry, I'm afraid I have to agree with you to an extent - I certainly think the combination of Validator and Attribute is a bit cumbersome. I think the problem stems from the fact that the Data Annotations attributes were designed for Dynamic Data etc, and have been reused. If the attribute's IsValid method took some context information and the entity being validated all would be saved. If you look at the attribute's methods in later versions of the framework it looks like that has been added, so I'm hopeful MVC might make use of that in the future -- but I don't have any information to that effect. Alternatively, my own preference would be that the attribute didn't have any isvalid method at all, and all that be done by the validator, with context etc. Then the attribute is purely metadata. Once you've read this post though it should be clear enough, and it certainly works very well in practice! SimonAnonymous
July 08, 2010
The comment has been removedAnonymous
September 07, 2010
Hello, is there a sample for clientside validation? DominicAnonymous
September 12, 2010
@ Dominic, I'd recommend checking out some of my colleague and fellow MVC fan's blog; blogs.msdn.com/.../stuartleeks Stuart really knows his stuff and has some nice validation posts, including talking about how MVC 3 changes the code. In particular this post and it's follow-up; blogs.msdn.com/.../asp-net-mvc-adding-client-side-validation-to-propertiesmustmatchattribute.aspx SimonAnonymous
October 25, 2010
Uh, wait, I take back what i said. In your sample project you have: if ((value == null && Attribute.TargetValue == null) || (value.Equals(Attribute.TargetValue))) but in the post here you have: if ((value == null && Attribute.TargetValue == null) || (value != null && value.Equals(Attribute.TargetValue))) Which is what I was suggesting. So cheers and all that.Anonymous
October 26, 2010
@ Merritt - oops, sorry! Not sure how that happened! SimonAnonymous
January 14, 2011
How would you validate the property manually? For example, if I put the attribute on a property for a model in services and wanted to validate it before saving it, how would I go about that? In my attempts to find an answer, I have run across many solutions that validate attributes, but they all seem to use the Attribute's IsValid() method - not the Validator's Validate() method. Thanks!Anonymous
January 16, 2011
@ Matt, good question. Have a look at my post here, that basically rips off some MVC internal code to call the validators; blogs.msdn.com/.../view-model-versus-domain-entity-validation-with-mvc.aspx ... having said that, if I were you I'd look at the new features in MVC 3. The addition of a ValidationContext to the Validation Attribute's IsValid method means this conditional validation task is way simpler now. SimonAnonymous
January 17, 2011
Simon, It would be great if you could update this post to use MVC3Anonymous
February 06, 2011
@ Matt, your wish is granted :-) blogs.msdn.com/.../conditional-validation-in-asp-net-mvc-3.aspx SimonAnonymous
March 23, 2011
great example, thanks but i think the limitation is only one RequiredIf attribute can be specified per "City". If I want to specify required if isUK, or US or Canada Citizen, then I hit the limitation. I think eventually you need to turn to jquery validation when things are too complicated.Anonymous
March 23, 2011
@ Henry; there is nothing preventing you from enhancing this attribute to look at multiple properties, or multiple values of a single property. If you use jQuery.validate this only runs in the browser - you should always repeat validation on the server to avoid potential security and consistency problems. The approach in this post uses MVC infrastructure combined with jQuery.validate to do both, whilst keeping the metadata about that validation in a single place. SimonAnonymous
June 23, 2011
What if you have 2 fields that rely on each other, i.e. if Field A is not null, then Field B is required, and if Field B is not null, then Field A is required. How would you set up the [RequiredIf(...)] statement, as far as the 2nd parameter?Anonymous
June 23, 2011
Would I be able to use this if I had 2 fields that were required if and only if each of them were not null? If so, can you tell me how I might set that up? I apologize if my comment gets posted more than once here.Anonymous
August 30, 2011
It's working for me, with the exception of the attribute's ErrorMessage not appearing in my view.Anonymous
August 30, 2011
I can't seem to get ErrorMessages to appear in my view when using RequiredIf. I'm not sure where the problem lies, as I'm a bit of a newbie with MVC's validation system. I have a more detailed account of my problem over at Stack Overflow: stackoverflow.com/.../399584 The RequiredIf code is exactly as you had it in the example solution (with comments removed in the attribute itself for brevity), so I'm thinking the problem is in how I'm using it.Anonymous
October 07, 2011
The comment has been removedAnonymous
October 07, 2011
Hi Simon, Somehow jQuery 1.6 + does returns null/ undefined when doing control.attr('checked'), so the toString() errors out. I have to replace control.attr('checked').toString() with control.is(':checked').toString() This works in all major browsers, jQuery 1.6 and up. CheersAnonymous
October 10, 2011
@ Tej & Tarinder, thanks for the comments - I'll try to look at them once I've worked out if I can get this code "out there" properly. Tej - yours does sound very odd, I've not seen that before. Did you resolve it? Tarinder - good spot, and yes another person had reported the same. I'll look to update it soon. SimonAnonymous
July 16, 2012
Thank you very much.I searched a lot for this but couldn't find.You are hero.Anonymous
July 04, 2013
I am using JQuery.validate.min.js and JQuery.validate.unobstrusive.min.js. For other control, I am just calling @Html.ValidationMessageFor(m=>m.PropertyName,"Message") If I wants to client side validation using RequiredIf attribute for conditional Validation. What needs to be done? Please help me. Thanks, Ujjwal KolkataAnonymous
July 04, 2013
@Simon J Ince I am using JQuery.validate.min.js and JQuery.validate.unobstrusive.min.js. For other control, I am just calling @Html.ValidationMessageFor(m=>m.PropertyName,"Message") and it is working as expected. If I wants to client side validation using RequiredIf attribute for conditional Validation. What needs to be done? Please help me. Thanks, Ujjwal Kolkata