Borrowing heavily from the responses from Alexander Gore and Jaime Marín in a related StackOverflow question1, I created five classes that enable comparing two fields in the same model using GT, GE, EQ, LE, and LT operators, provided they implement IComparable. So it can be used for pairs of dates, times, integers, and strings, for example.
It would be nice to merge these all into one class and take the operator as an argument, but I don't know how. I left the three exceptions as is because, if thrown, they really represent a form design problem, not a user input problem.
You just use it in your model like this, and the file with the five classes follows:
[Required(ErrorMessage = "Start date is required.")]
public DateTime CalendarStartDate { get; set; }
[Required(ErrorMessage = "End date is required.")]
[AttributeGreaterThanOrEqual("CalendarStartDate",
ErrorMessage = "The Calendar end date must be on or after the Calendar start date.")]
public DateTime CalendarEndDate { get; set; }
using System;
using System.ComponentModel.DataAnnotations;
//Contains GT, GE, EQ, LE, and LT validations for types that implement IComparable interface.
//https://stackoverflow.com/questions/41900485/custom-validation-attributes-comparing-two-properties-in-the-same-model
namespace DateComparisons.Validations
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeGreaterThan : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeGreaterThan(string comparisonProperty){_comparisonProperty = comparisonProperty;}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value==null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if(!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) > 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeGreaterThanOrEqual : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeGreaterThanOrEqual(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) >= 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeEqual : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeEqual(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) == 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeLessThanOrEqual : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeLessThanOrEqual(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) <= 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeLessThan : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeLessThan(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) < 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
}