OdeToCode IC Logo

Custom Data Annotation Validator Part I : Server Code

Tuesday, February 22, 2011

Let's say you want to create a GreaterThan validation attribute and use it like so:

public class Trip
{
    [Required]        
    public DateTime StartDate { get; set; }
    
    [Required]
    [GreaterThan("StartDate")]
    public DateTime EndDate { get; set; }
}

The implementation would look something like this:

public class GreaterThanAttribute : ValidationAttribute
{
    public GreaterThanAttribute(string otherProperty)
        :base("{0} must be greater than {1}")
    {
        OtherProperty = otherProperty;
    }

    public string OtherProperty { get; set; }

    public override string FormatErrorMessage(string name)
    {            
        return string.Format(ErrorMessageString, name, OtherProperty);
    }

    protected override ValidationResult 
        IsValid(object firstValue, ValidationContext validationContext)
    {
        var firstComparable = firstValue as IComparable;
        var secondComparable = GetSecondComparable(validationContext);

        if (firstComparable != null && secondComparable != null)
        {
            if (firstComparable.CompareTo(secondComparable) < 1)
            {
                return new ValidationResult(
                    FormatErrorMessage(validationContext.DisplayName));
            }
        }               

        return ValidationResult.Success;
    }        

    protected IComparable GetSecondComparable(
        ValidationContext validationContext)
    {
        var propertyInfo = validationContext
                              .ObjectType
                              .GetProperty(OtherProperty);
        if (propertyInfo != null)
        {
            var secondValue = propertyInfo.GetValue(
                validationContext.ObjectInstance, null);
            return secondValue as IComparable;
        }
        return null;
    }
}

Notice the IsValid override available in .NET 4 adds a ValidationContext typed parameter, and this parameter makes it easier to look "outside the attribute" at the associated model type and model instance (validationContext.ObjectType.GetProperty(OtherProperty)). 

The validation context does have a shortcoming when it comes to model metadata, however. The context brings along the DisplayName value for the current property, but there is nothing available to easily get to metadata for the rest of the model. For example, when formatting the error string it would be nice to use the friendly name of both properties involved in validation, but we only have DisplayName for one. One possible solution is to dig it our from the model metadata provider.

protected string GetOtherDisplayName(ValidationContext validationContext)
{
    var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(
        null, validationContext.ObjectType, OtherProperty);
    if (metadata != null)
    {
        return metadata.GetDisplayName();
    }
    return OtherProperty;
}

Next up: adding client validation and using jQuery.validate.