Custom Validation Attributes: Comparing two properties in the same model

You can create a custom validation attribute for comparison two properties. It's a server side validation:

public class MyViewModel
{
    [DateLessThan("End", ErrorMessage = "Not valid")]
    public DateTime Begin { get; set; }

    public DateTime End { get; set; }
}

public class DateLessThanAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public DateLessThanAttribute(string comparisonProperty)
    {
         _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;
        var currentValue = (DateTime)value;

        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);

        if (property == null)
            throw new ArgumentException("Property with this name not found");

        var comparisonValue = (DateTime)property.GetValue(validationContext.ObjectInstance);

        if (currentValue > comparisonValue)
            return new ValidationResult(ErrorMessage);

        return ValidationResult.Success;
    }
}

Update: If you need a client side validation for this attribute, you need implement an IClientModelValidator interface:

public class DateLessThanAttribute : ValidationAttribute, IClientModelValidator
{
    ...
    public void AddValidation(ClientModelValidationContext context)
    {
        var error = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
        context.Attributes.Add("data-val", "true");
        context.Attributes.Add("data-val-error", error);
    }
}

The AddValidation method will add attributes to your inputs from context.Attributes.

enter image description here

You can read more here IClientModelValidator


Based on the Alexander Gore response, I suggest a better and generic validation (and it is .Net core compatible). When you want to compare properties using GreatherThan or LessThan logic (whatever the types are), you could validate if they have implemented the IComparable interface. If both properties are valid you could use the CompareTo implementation. This rule applies for DateTime and number types as well

LessThan

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class LessThanAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public LessThanAttribute(string comparisonProperty)
    {
        _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        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 (comparisonValue.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("Comparison property has not implemented IComparable interface");
        }

        if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
        {
            throw new ArgumentException("The properties types must be the same");
        }

        if (currentValue.CompareTo((IComparable)comparisonValue) >= 0)
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }
}

GreaterThan

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class GreaterThanAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public GreaterThanAttribute(string comparisonProperty)
    {
        _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        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 (comparisonValue.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("Comparison property has not implemented IComparable interface");
        }

        if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
        {
            throw new ArgumentException("The properties types must be the same");
        }

        if (currentValue.CompareTo((IComparable)comparisonValue) < 0)
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }
}

In a booking context an example could be as follow:

public DateTime CheckInDate { get; set; }

[GreaterThan("CheckInDate", ErrorMessage = "CheckOutDate must be greater than CheckInDate")]
public DateTime CheckOutDate { get; set; }

I created a library with the most common custom validations in ASP.NET Core. The library also has the client-side validation for all the server-side custom validations. The library solves OP's problem with a single attribute as follows:

// If you want the StartDate to be smaller than the EndDate:
[CompareTo(nameof(EndDate), ComparisionType.SmallerThan)] 
public DateTime StartDate { get; set; }

Here is the GitHub link of the library: AspNetCore.CustomValidation

Currently, the library contains the following validation attributes:

1. FileAttribute - To validate file type, file max size, file min size;

2. MaxAgeAttribute - To validate maximum age against date of birth value of DateTime type;

3. MinAgeAttribute - To validate minimum required age against a date of birth value of DateTime type;

4. MaxDateAttribute -To set max value validation for a DateTime field;

5. MinDateAttribute - To set min value validation for a DateTime field;

6. CompareToAttibute – To compare one property value against another property value;

7. TinyMceRequiredAttribute -To enforce required validation attribute on the online text editors like TinyMCE, CkEditor, etc.


As one possible option self-validation:

You just need to Implement an interface IValidatableObject with the method Validate(), where you can put your validation code.

public class MyViewModel : IValidatableObject
{
    [Required]
    public DateTime StartDate { get; set; }

    [Required]
    public DateTime EndDate { get; set; } = DateTime.Parse("3000-01-01");

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        int result = DateTime.Compare(StartDate , EndDate);
        if (result < 0)
        {
            yield return new ValidationResult("start date must be less than the end date!", new [] { "ConfirmEmail" });
        }
    }
}