Validate a value based on the value of another property

Validate a value based on the value of another property

In this post, I will show how to create a helper to validate the length of a property based on the value of another one, to be precise, to check if the country selected by the user is a specific value (“Italy” for this example) and then check that province property should be no more than two characters long. In all other cases, it should be no more than 50 characters long. The check had to be performed client side, for a better user experience, and server side to be sure the data is valid and not altered from a crafted call. The check in question is a sample; you will see that you can easily adapt the same code to different types of constraints.

In the end, we will use the attribute in a ViewModel like this:

public class ContactForm
{
    [Required]
    [Display(Name = "First name")]
    public string FirstName { get; set; }

    [Required]
    [Display(Name = "Last name")]
    public string LastName { get; set; }

    [Required]
    [Display(Name = "Country")]
    public string CountryCode { get; set; }

    public SelectList CountriesList { get; set; }

    [Required]
    [Display(Name = "Province")]
    [LengthOnOtherPropertyValue("CountryCode", "ITA", 2, 50, ErrorMessage = "{0} can be {1} chars long")]
    public string Province { get; set; }
}

Starting from the server side

We start preparing our attribute by deriving from ValidationAttribute and creating the constructor with all the parameters that we need. Precisely, the name of the property to match, the value to check, the maximum length in case of match and the maximum length for the other cases.

public class LengthOnOtherPropertyValueAttribute : ValidationAttribute
{
  private readonly string propertyNameToCheck;
  private readonly string propertyValueToCheck;
  private readonly int maxLengthOnMatch;
  private readonly int maxLength;

  public LengthOnOtherPropertyValueAttribute(string propertyNameToCheck, string propertyValueToCheck, int maxLengthOnMatch, int maxLength)
  {
    this.propertyValueToCheck = propertyValueToCheck;
    this.maxLengthOnMatch = maxLengthOnMatch;
    this.maxLength = maxLength;
    this.propertyNameToCheck = propertyNameToCheck;
  }
}

Now we change the behavior of the method IsValid using our logic:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
  var propertyName = validationContext.ObjectType.GetProperty(propertyNameToCheck);
  if (propertyName == null)
    return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "Unknown property {0}", new[] { propertyNameToCheck }));

  var propertyValue = propertyName.GetValue(validationContext.ObjectInstance, null) as string;

  if (propertyValueToCheck.Equals(propertyValue, StringComparison.InvariantCultureIgnoreCase) && value != null && ((string)value).Length > maxLengthOnMatch)
    return new ValidationResult(string.Format(ErrorMessageString, validationContext.DisplayName, maxLengthOnMatch));

  if (value != null && ((string)value).Length > maxLength)
    return new ValidationResult(string.Format(ErrorMessageString, validationContext.DisplayName, maxLength));

  return ValidationResult.Success;
}

The code should be self-explanatory; we get the property’s name and value from the validation context and check if the length respect our rules:

  • If propertyValueToCheck is equal to propertyValue, the length should not be more than maxLengthOnMatch
  • If propertyValueToCheck is not equal to propertyValue, the length should not be more than maxLength

Note that we use ErrorMessageString from ValidationAttribute to compose the error message, so we have support for Resources without doing nothing. In case you want to use resources, the implementation will be like this:

[Required]
[Display(Name = "Province")]
[LengthOnOtherPropertyValue("CountryCode", "ITA", 2, 50, ErrorMessageResourceType = typeof(MyResource), ErrorMessageResourceName = "LengthOnOtherPropertyValueErrorMessage")]
public string Province { get; set; }

If we use the attribute, everything will works fine, the code will be called on every form post and, in the method of our controller, we just need to check ModelState.IsValid to know if the model is correctly filled or not. Nice, but the page will reload also if the data is invalid and it is not too nice from a user point of view. We need to implement a client side validation to prevent this behavior. Just a little note before moving to the next step, the client side validation is good for the user experience (and to reduce load and wait time), but remember to always check, server side, that data is correct and valid, you will never know if the call was tampered or not.

Implementing unobtrusive validation in the client side

First thing, we should add a dependency from the interface IClientValidatable, so that the framework know we also support client side validation. To respect the contract we then implement the GetClientValidationRules method

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
  var rule = new ModelClientValidationRule
  {
    ErrorMessage = string.Format(ErrorMessageString, metadata.GetDisplayName(), maxLengthOnMatch),
    ValidationType = "lengthonotherpropertyvalue"
  };
  rule.ValidationParameters.Add("nametocheck", propertyNameToCheck);
  rule.ValidationParameters.Add("valuetocheck", propertyValueToCheck);
  rule.ValidationParameters.Add("maxlengthonmatch", maxLengthOnMatch);
  rule.ValidationParameters.Add("maxlength", maxLength);
  yield return rule;
}

It should be clear enough what we are doing; we are preapring a ValidationRule with the error message and all the values needed to perform our validation. Note that the params name must be lower case because is the rule of HTML5. The data-* attributes can be lower case letters, numbers, and dashes only.

The rendered Html will be like this:

<input data-val="true" 
    data-val-lengthonotherpropertyvalue="Province can be 2 chars long" 
    data-val-lengthonotherpropertyvalue-maxlength="50" 
    data-val-lengthonotherpropertyvalue-maxlengthonmatch="2" 
    data-val-lengthonotherpropertyvalue-nametocheck="CountryCode" 
    data-val-lengthonotherpropertyvalue-valuetocheck="ITA" 
    data-val-required="Field Province is required." 
    id="Province" 
    name="Province" 
    type="text" 
    value="" />
<span class="field-validation-valid" 
    data-valmsg-for="Province" 
    data-valmsg-replace="true">
</span>

As you can see, the framework has automatically rendered all the data-val-lengthonotherpropertyvalue-* attributes that we need. To complete the work, we create the equivalent rule in a JavaScript file (or a script code block) and load it into our view (or in the layout).

(function ($) {
  jQuery.validator.addMethod('lengthonotherpropertyvalue',
    function (value, element, params) {
      var $property = $('#' + params.nametocheck);
      var propertyValue = $property.val();
      if (propertyValue === params.valuetocheck && value.length > params.maxlengthonmatch)
        return false;
      if (propertyValue !== params.valuetocheck && value.length > params.maxlength)
        return false;
      return true;
    });

  jQuery.validator.unobtrusive.adapters.add('lengthonotherpropertyvalue',
    ['nametocheck', 'valuetocheck', 'maxlength', 'maxlengthonmatch'],
    function (options) {
      options.rules['lengthonotherpropertyvalue'] = {
        nametocheck: options.params.nametocheck,
        valuetocheck: options.params.valuetocheck,
        maxlength: options.params.maxlength,
        maxlengthonmatch: options.params.maxlengthonmatch
      };
      options.messages['lengthonotherpropertyvalue'] = options.message;
    });
})(jQuery);

Notice that the code is not in the DOM Ready, because it will be too late in the execution. We need to register the jQuery.validator.unobtrusive.adapters before the jQuery.unobtrusive plugin call its method parse(document). The rest of the code is straightforward, we created a custom adapter with the same parameters, rules and error message we declared previously in the CS file. We then added a method to do the check in the same way we did in our C# class.

Sample code

You can find the complete code with a sample project here. If you have grasped the concepts exposed here, it should be much easier to write your own custom validator now. Happy coding!