Writing a CompareTo DataAnnotation Attribute

I have a situation where I want to compare to fields (example, ensuring the start time is before the end time). I'm using the System.ComponentModel.DataAnnotations attributes for my validation.

My first thought was something like this:

public enum CompareToOperation
{
    EqualTo,
    LessThan,
    GreaterThan
}

public class CompareToAttribute : ValidationAttribute
{
    CompareToOperation _Operation;
    IComparable _Comparision;

    public CompareToAttribute(CompareToOperation operation, Func<IComparable> comparison)
    {
       _Operation = operation;
       _Comparision = comparison();
    }

    public override bool IsValid(object value)
    {
    if (!(value is IComparable))
        return false;

    switch (_Operation)
    {
        case CompareToOperation.EqualTo: return _Comparision.Equals(value);
        case CompareToOperation.GreaterThan: return _Comparision.CompareTo(value) == 1;
        case CompareToOperation.LessThan: return _Comparision.CompareTo(value) == -1;
    }

    return false;
    }
}

public class SimpleClass
{
   public DateTime Start {get;set;}
   [CompareTo(CompareToOperation.GreaterThan, () => this.Start)] // error here
   public DateTime End {get;set;}
}

This doesn't work however, there's a compiler error where the attribute is marked:

Expression cannot contain anonymous methods or lambda expressions

Does anyone have a solution for this? Or a different approach for validating one field compared to the value of another?


Solution 1:

Check The AccountMOdel in the default project of MVC2, There is an attribute PropertiesMustMatchAttribute applied to the ChangePasswordModel to validate that the NewPassword and ConfirmPassword Match

   [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class PropertiesMustMatchAttribute : ValidationAttribute
{
    private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";

    private readonly object _typeId = new object();

    public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
        : base(_defaultErrorMessage)
    {
        OriginalProperty = originalProperty;
        ConfirmProperty = confirmProperty;
    }

    public string ConfirmProperty
    {
        get;
        private set;
    }

    public string OriginalProperty
    {
        get;
        private set;
    }

    public override object TypeId
    {
        get
        {
            return _typeId;
        }
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
            OriginalProperty, ConfirmProperty);
    }

    public override bool IsValid(object value)
    {
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
        object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
        object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
        return Object.Equals(originalValue, confirmValue);
    }
}

Solution 2:

A very ugly way that's not nearly as flexible is to put it on the class and use reflection. I haven't tested this, so I'm not actually sure it works, but it does compile :)

public enum CompareToOperation
{
    EqualTo,
    LessThan,
    GreaterThan
}

public class CompareToAttribute : ValidationAttribute
{
    CompareToOperation _Operation;
    string _ComparisionPropertyName1;
    string _ComparisionPropertyName2;

    public CompareToAttribute(CompareToOperation operation, string comparisonPropertyName1, string comparisonPropertyName2)
    {
        _Operation = operation;
        _ComparisionPropertyName1 = comparisonPropertyName1;
        _ComparisionPropertyName2 = comparisonPropertyName2;
    }

    private static IComparable GetComparablePropertyValue(object obj, string propertyName)
    {
        if (obj == null) return null;
        var type = obj.GetType();
        var propertyInfo = type.GetProperty(propertyName);
        if (propertyInfo == null) return null;
        return propertyInfo.GetValue(obj, null) as IComparable;
    }

    public override bool IsValid(object value)
    {
        var comp1 = GetComparablePropertyValue(value, _ComparisionPropertyName1);
        var comp2 = GetComparablePropertyValue(value, _ComparisionPropertyName2);

        if (comp1 == null && comp2 == null)
            return true;

        if (comp1 == null || comp2 == null)
            return false;

        var result = comp1.CompareTo(comp2);

        switch (_Operation)
        {
            case CompareToOperation.LessThan: return result == -1;
            case CompareToOperation.EqualTo: return result == 0;
            case CompareToOperation.GreaterThan: return result == 1;
            default: return false;
        }
    }
}

[CompareTo(CompareToOperation.LessThan, "Start", "End")]
public class SimpleClass
{
    public DateTime Start { get; set; }
    public DateTime End { get; set; }
}