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;

  1. Create an instance of the model type that is required
  2. Loop through all properties
    1. Perform required field validation on the current property
    2. Try to set the value of a property from the value providers
    3. If that fails log an error in ModelState
  3. When all properties have been processed;
    1. Get a composite validator (that basically does as follows)
    2. Loop through all properties
      1. Get a list of validators
        1. Call validate on each one
        2. If validation fails log an error in ModelState

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 (23)

  1. Barry Kelly says:

    That sounds amazingly complicated for such simple behaviour. Once upon a time (Avaeon.com, Topoix), I was working on a framework, built in .net, and designed for data entry and validation. A conditional requirement like yours would be as simple as "Required = this.IsUKResident"; might also be e.g. "Required = this.Country == 'UK'". But it was a declarative framework built around data flow (think: spreadsheets), where every property had a possibly non-constant bound expression. That meant that when other fields of the entity were updated, the property changes were propagated automatically (ultimately through AJAX to the web front end). It also wasn't specifically OO, but that's a different matter.

  2. Simon J Ince says:

    @ 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!

    Simon

  3. mP says:

    Expressing conditions and branch logic in attributes seems well seems like the kind of stuff that should  go in a proper langage. If your crazy enuff to use validations to such an extent where does the madness stop between validation and the rest of the app ?

  4. Dominic says:

    Hello,

    is there a sample for clientside validation?

    Dominic

  5. Simon J Ince says:

    @ 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

    Simon

  6. Merritt says:

    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.

  7. Simon J Ince says:

    @ Merritt –

    oops, sorry! Not sure how that happened!

    Simon

  8. Matt says:

    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!

  9. Simon J Ince says:

    @ 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.

    Simon

  10. Matt Frear says:

    Simon,

    It would be great if you could update this post to use MVC3

  11. Henry Mao says:

    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.

  12. Simon J Ince says:

    @ 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.

    Simon

  13. Mike says:

    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?

  14. Mike says:

    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.

  15. Kevin Major says:

    It's working for me, with the exception of the attribute's ErrorMessage not appearing in my view.

  16. Kevin Major says:

    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.  

  17. Tej Sarao says:

    Thanks for the excellent piece of work.

    However, when I use the RequiredIf attribute in my code and the property under question validates( returns true), the Client Side validation for other fields in the form does not run i.e. The form is posted back even though the other fields on the page are invalid. If I remove the field with RequiredIf attribute, everything is back to normal. If RequiredIf field is invalid, the client side validation runs fine.

    Any Ideas?

  18. Tajinder Sarao says:

    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.

    Cheers

  19. Simon J Ince says:

    @ 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.

    Simon

  20. Dhaval says:

    Thank you very much.I searched a lot for this but couldn't find.You are hero.

  21. Ujjwal Das says:

    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

    Kolkata

  22. Ujjwal das says:

    @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