Multi-property Custom Validation Attributes

I am going to expand on last week’s Simple Custom Validation Attributes with custom validation attributes that need to look at more than a single property of a model. Again using the contact application example, lets say that if a contact has a country of USA then state and zip code are required.

As before we have class which inherits from ValidationAttribute. IsValid is still overridden, but it is a different overload than the previous example that provides access to the ValidationContext, which in our case is the contact model that is being validated.

using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

public class ValidCountryRequired : ValidationAttribute
{
    private readonly string _countryProperty;
    private readonly string[] _requriedByCountries;

    public ValidCountryRequired(string countryProperty,
                                string[] requriedByCountries)
    {
        _countryProperty = countryProperty;
        _requriedByCountries = requriedByCountries;
    }

    protected override ValidationResult IsValid(object value,
                                                ValidationContext context)
    {
        PropertyInfo propertyInfo;
        propertyInfo = context.ObjectType.GetProperty(_countryProperty);
        if (propertyInfo == null)
        {
            return new ValidationResult(_countryProperty +
                                        " does not exist");
        }

        var country = propertyInfo.GetValue(context.ObjectInstance, null);

        if (_requriedByCountries.Contains(country.ToString()) &&
            (value == null ||
             String.IsNullOrWhiteSpace(value.ToString())))
        {
            return new ValidationResult(context.DisplayName +
                                        " is requried for contacts from " +
                                        country);
        }

        return null;
    }
}

For this example I have added a constructor that takes the name of the country property in the model being validated and a string array of the countries that need to make sure the state and zip code are filled in. Both parameters are stored in class level variables for use in the IsValid function.

The IsValid function is still where all the work takes place. The overload of IsValid we are working with provides the value of the property being validated and the validation context that validation is running for. The validation context will allow us to access other properties on the model being validated.

The first step to checking a different property than the one being validated is to get the property info from the validation context by using context.ObjectType.GetProperty and passing the property name that was passed in via the constructor. If GetProperty returns null then the context did not contain the property that was expected. In this case a new ValidationResult is returned with a message that the expected country property was not found.

Using the property info for the country property we can call GetValue with context.ObjectInstance to get the contact’s country. If that country is found in the string array of countries that require the property being validated then a check is run to make sure the property has a value and is not blank.

If the property does not have a value then a new ValidationResult is returned with an error message. If the property does have a value then null is returned indicating that the property is valid.

Example usage from my contact model:

[ValidCountryRequired("Country", new[] { "USA" })]
public string State { get; set; }
[Display(Name = "Zip Code")]
[ValidCountryRequired("Country", new[] { "USA" })]
public string ZipCode { get; set; }

One other thing to note is the usage of context.DisplayName when creating the ValidationResult. By using display name in the message the ZipCode property will show as “Zip Code” in the message returned.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.