Custom ValidationAttribute for comparing properties.

We’re using ASP.NET MVC3 on our latest project and have been really impressed so far.  One of the coolest things I have come across is using DataAnnotations to decorate the model classes and having the framework wire up all the validation and error messages.  It is truly great – you can find out more at Scott Guthrie’s blog post on the subject (for MVC2 but the principles are the same).

There’s no built in support for comparing two properties though this has been addressed to a certain extent by the CompareAttribute in the System.Web.Mvc namespace in MVC3, David Hayden has blogged about it here.  This attribute allows you to specify that the value of two properties should be equal, but doesn’t help with greater than or less than operations.  This kind of thing would be really useful to specify that the StartDate of an Appointment should be before the EndDate.

So I have written my own custom comparison validation attribute for use with asp.net mvc in the style of the built in data annotations.

    /// <summary>
    /// Specifies that the field must compare favourably with the named field, if objects to check are not of the same type
    /// false will be return
    /// </summary>
    [AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
    public class CompareValuesAttribute : ValidationAttribute
    {
        /// <summary>
        /// The other property to compare to
        /// </summary>
        public string OtherProperty { get; set; }

        public CompareValues Criteria { get; set; }

        /// <summary>
        /// Creates the attribute
        /// </summary>
        /// <param name="otherProperty">The other property to compare to</param>
        public CompareValuesAttribute(string otherProperty, CompareValues criteria)
        {
            if (otherProperty == null)
                throw new ArgumentNullException("otherProperty");

            OtherProperty = otherProperty;
            Criteria = criteria;
        }

        /// <summary>
        /// Determines whether the specified value of the object is valid.  For this to be the case, the objects must be of the same type
        /// and satisfy the comparison criteria. Null values will return false in all cases except when both
        /// objects are null.  The objects will need to implement IComparable for the GreaterThan,LessThan,GreatThanOrEqualTo and LessThanOrEqualTo instances
        /// </summary>
        /// <param name="value">The value of the object to validate</param>
        /// <param name="validationContext">The validation context</param>
        /// <returns>A validation result if the object is invalid, null if the object is valid</returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // the the other property
            var property = validationContext.ObjectType.GetProperty(OtherProperty);

            // check it is not null
            if (property == null)
                return new ValidationResult(String.Format("Unknown property: {0}.", OtherProperty));

            // check types
            if (validationContext.ObjectType.GetProperty(validationContext.MemberName).PropertyType != property.PropertyType)
                return new ValidationResult(String.Format("The types of {0} and {1} must be the same.", validationContext.DisplayName, OtherProperty));

            // get the other value
            var other = property.GetValue(validationContext.ObjectInstance, null);

            // equals to comparison,
            if (Criteria == CompareValues.EqualTo)
            {
                if (Object.Equals(value, other))
                    return null;
            }
            else if (Criteria == CompareValues.NotEqualTo)
            {
                if (!Object.Equals(value, other))
                    return null;
            }
            else
            {
                // check that both objects are IComparables
                if (!(value is IComparable) || !(other is IComparable))
                    return new ValidationResult(String.Format("{0} and {1} must both implement IComparable", validationContext.DisplayName, OtherProperty));

                // compare the objects
                var result = Comparer.Default.Compare(value, other);

                switch (Criteria)
                {
                    case CompareValues.GreaterThan:
                        if (result > 0)
                            return null;
                        break;
                    case CompareValues.LessThan:
                        if (result < 0)
                            return null;
                        break;
                    case CompareValues.GreatThanOrEqualTo:
                        if (result >= 0)
                            return null;
                        break;
                    case CompareValues.LessThanOrEqualTo:
                        if (result <= 0)
                            return null;
                        break;
                }
            }

            // got this far must mean the items don't meet the comparison criteria
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }

        /// <summary>
        /// Applies formatting to an error message.
        /// </summary>
        /// <param name="name">The name to include in the error message</param>
        /// <returns></returns>
        public override string FormatErrorMessage(string name)
        {
            return String.Format(CultureInfo.CurrentCulture, base.ErrorMessageString, name, OtherProperty, Criteria.Description());
        }

        /// <summary>
        /// retrieve the object to compare to
        /// </summary>
        /// <returns></returns>
        object GetOther(ValidationContext context)
        {
            return null;
        }
    }

    /// <summary>
    /// Indicates a comparison criteria used by the CompareValues attribute
    /// </summary>
    public enum CompareValues
    {
        [Description("=")]
        EqualTo,
        [Description("!=")]
        NotEqualTo,
        [Description(">")]
        GreaterThan,
        [Description("<")]
        LessThan,
        [Description(">=")]
        GreatThanOrEqualTo,
        [Description("<=")]
        LessThanOrEqualTo
    }

To use this attribute one would then just do:

    /// <summary>
    /// Represents a booking
    /// </summary>
    public class Booking : DomainObject<int>
    {
        /// <summary>
        /// The start of the booking
        /// </summary>
        [Required]
        [CompareValues("End", CompareValues.LessThan)]
        public virtual DateTime? Start { get; set; }

        /// <summary>
        /// The end of the booking
        /// </summary>
        [Required]
        public virtual DateTime? End { get; set; }
    }

4 Responses to “Custom ValidationAttribute for comparing properties.”

  1. John says:

    I’m getting an error on Criteria.Description():

    ‘MyProject.Models.CompareValues’ does not contain a definition for ‘Description’ and no extension method ‘Description’ accepting a first argument of type ‘MyProject.Models.CompareValues’ could be found (are you missing a using directive or an assembly reference?)

    Here are my imports:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.ComponentModel.DataAnnotations;
    using System.Collections;
    using System.Globalization;
    using System.ComponentModel;

  2. peter says:

    John, sorry for the massive delay responding, for some reason we’re not seeing the comments come in.

    Anyway, thanks for pointing that out, Description() is one of our extension methods in another library.

    Here is the complete code:

    
    using System;
    using System.ComponentModel;
    
    namespace ConcurrentDevelopment.Extensions
    {
        /// <summary>
        /// Extension methods for enums
        /// </summary>
        public static class EnumExtensions
        {
            /// <summary>
            /// Get the description attribute for the enum
            /// </summary>
            /// <param name="e"></param>
            /// <returns></returns>
            public static string Description(this Enum e)
            {
                var da = (DescriptionAttribute[])(e.GetType().GetField(e.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false));
    
                return da.Length > 0 ? da[0].Description : e.ToString();
            } 
    
        }
    }
    
    
  3. Fancy says:

    How does one implement Icomparable for int?

Leave a Reply