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.


Comments
Maarten van der lee Tuesday, March 1, 2011
Isn't it possible to find the friendly name or other attributes, by using reflection? You already used reflection to get the value of the property.
gravatar scott Tuesday, March 1, 2011
@Maarten: Yes - could use reflection. If you are on a project that is using a custom metadata provider and not the attribute based provider, then the above code would still work.
gravatar Zote Wednesday, March 2, 2011
@Maarten: watch @bradwilson's Advanced ASP.NET MVC 3 Presentation. It's same sample, and with what you need.

In with url, https://bitbucket.org/zote/validacaomvc, I have this and others validators.
Doeke Wednesday, March 23, 2011
I would rename OtherProperty to OtherPropertyName. I'm not familiar with these validation attributes. I also would start the article with some indication to which library it's about ;-)
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!