Skip to content

Instantly share code, notes, and snippets.

@Vovanda
Last active April 9, 2025 00:54
Show Gist options
  • Save Vovanda/c4265f3308649b43104707118c46e892 to your computer and use it in GitHub Desktop.
Save Vovanda/c4265f3308649b43104707118c46e892 to your computer and use it in GitHub Desktop.
Реализация CompareTo в качестве атрибута валидации

Атрибут для сравнения значения свойства с другим свойством модели.

Пример использования

Класс с валидацией для бронирования

public record Reservation
{
    [Required]
    public DateTime CheckInDate { get; init; }

    [Required]
    [CompareTo(nameof(CheckInDate), Comparison.GreaterOrEqual, ErrorMessage = "Дата выезда должна быть позже или равна дате заезда.")]
    public DateTime CheckOutDate { get; init; }
}

Описание

  • CheckInDate — дата заезда/начала.
  • CheckOutDate — дата выезда/окончания.
  • Атрибут CompareTo проверяет, что CheckOutDate больше или равна CheckInDate.

Сообщение об ошибке

Если условие не выполняется, выводится сообщение об ошибке, например:

"Дата выезда должна быть позже или равна дате заезда."

Доступные типы сравнения

  • Less — меньше.
  • Equal — равно.
  • Greater — больше.
  • LessOrEqual — меньше или равно.
  • GreaterOrEqual — больше или равно.
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace ComponentModel.DataAnnotations;
/// <summary>
/// Атрибут для сравнения значения свойства с другим свойством модели
/// </summary>
/// <remarks>
/// Примеры использования:
/// <code>
/// [CompareTo(nameof(Age), Comparison.GreaterOrEqual)]
/// [CompareTo(nameof(Password), Comparison.Equal, ErrorMessage = "Пароли не совпадают")]
/// </code>
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class CompareToAttribute : ValidationAttribute
{
private readonly string _otherProperty;
private readonly Comparison _comparison;
/// <summary>
/// Создает новый экземпляр атрибута сравнения
/// </summary>
/// <param name="otherProperty">Имя свойства для сравнения (используйте nameof())</param>
/// <param name="comparison">Тип сравнения</param>
public CompareToAttribute(string otherProperty, Comparison comparison)
{
_otherProperty = otherProperty;
_comparison = comparison;
}
/// <inheritdoc />
public override bool RequiresValidationContext => true;
/// <inheritdoc />
protected override ValidationResult? IsValid(
object? value,
ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_otherProperty)
?? throw new InvalidOperationException($"Свойство {_otherProperty} не найдено");
var otherValue = property.GetValue(validationContext.ObjectInstance);
var comparison = (value, otherValue) switch
{
(null, null) => Comparison.Equal,
(null, _) => Comparison.Less,
(_, null) => Comparison.Greater,
_ when value is not IComparable =>
throw new InvalidOperationException(
$"Тип {value.GetType().Name} не поддерживает сравнение"),
_ => CompareValues(value, otherValue!)
};
return (_comparison & comparison) == comparison
? ValidationResult.Success
: new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
private static Comparison CompareValues(object value, object otherValue)
{
try
{
return ((IComparable)value).CompareTo(otherValue) switch
{
< 0 => Comparison.Less,
0 => Comparison.Equal,
_ => Comparison.Greater
};
}
catch (ArgumentException)
{
throw new InvalidOperationException(
$"Невозможно сравнить {value.GetType().Name} и {otherValue.GetType().Name}");
}
}
/// <inheritdoc />
public override string FormatErrorMessage(string name) =>
!string.IsNullOrEmpty(ErrorMessage)
? string.Format(CultureInfo.CurrentCulture, ErrorMessage, name, _otherProperty)
: $"{name} должно быть {GetComparisonText()} {_otherProperty}.";
private string GetComparisonText() => _comparison switch
{
Comparison.Less => "меньше чем",
Comparison.Equal => "равно",
Comparison.Greater => "больше чем",
Comparison.NotEqual => "не равно",
Comparison.LessOrEqual => "меньше или равно",
Comparison.GreaterOrEqual => "больше или равно",
_ => throw new InvalidOperationException("Неподдерживаемый тип сравнения")
};
}
/// <summary>
/// Тип сравнения для атрибута CompareTo
/// </summary>
[Flags]
public enum Comparison
{
/// <summary>Меньше</summary>
Less = 1 << 0,
/// <summary>Равно</summary>
Equal = 1 << 1,
/// <summary>Больше</summary>
Greater = 1 << 2,
/// <summary>Не равно (Less | Greater)</summary>
NotEqual = Less | Greater,
/// <summary>Меньше или равно (Less | Equal)</summary>
LessOrEqual = Less | Equal,
/// <summary>Больше или равно (Greater | Equal)</summary>
GreaterOrEqual = Greater | Equal
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment