ASP.NET Web Forms には CompareValidator という検証コントロールがあって、2 つの TextBox に入力された値の大小比較をして検証できる機能があります。
それと同様な検証機能を ASP.NET MVC で実装する方法を書きます。(元の話は Teratail のスレッド ASP.NET MVC4 TimeSpan型入力値の検証です)(以下の記事は .NET Framework 版の MVC5 での例ですが、ASP.NET Core 3.1 MVC の場合も Model と検証属性のコードは同じです)

ASP.NET MVC には CompareAttribute という検証用のデータアノテーション属性がありますが、それには大小を比較する機能はなく、同じか否かを検証するのみです。
なので、例えば、ユーザーが開始時間と終了時間をテキストボックスに入力した際に、終了時間が開始時間より後になっていることを検証するような場合は、カスタム検証機能を実装することになります。
複数のプロパティにまたがった検証をする場合、Teratail のスレッド「MVC モデルのバリデーションについて」の回答に書いたように、CustomValidationAttribute を使うか、モデルに IValidatableObject インターフェイスを実装するという手段を使うのが普通のようです。
しかし、2 つのテキストボックス入力の大小比較程度なら、CustomValidationAttribute を使わなくても、リフレクションを使って比較対象のプロパティの値を取得し、それと自身のプロパティの値を比較して検証結果を返すということができます。
参考にしたのは Compare Validator in MVC という記事の Limitations of the Compare DataAnnotation 以下のセクションです。
上の画像を表示するのに使ったサンプルコードは以下の通りです。TimeSpan 型のプロパティを検証の対象としていますが、DataTime 型その他でもキャストを変更して同様に検証可能です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace Mvc5App.Models
{
public class KintaiModel
{
public IList<Record> Records { get; set; }
}
public class Record
{
public int Id { set; get; }
public string Week { set; get; }
public TimeSpan Open { set; get; }
[TimeGreaterThan("Open",
"{0} は Open より後に設定してください")]
public TimeSpan Close { set; get; }
// ・・・中略・・・
}
[AttributeUsage(AttributeTargets.Property)]
public class TimeGreaterThanAttribute : ValidationAttribute
{
private string _startTimePropertyName;
// コンストラクタ
public TimeGreaterThanAttribute(
string startTimePropertyName, string errorMsg)
{
this._startTimePropertyName = startTimePropertyName;
this.ErrorMessage = errorMsg;
}
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture,
ErrorMessageString, name);
}
protected override ValidationResult IsValid (
object value, ValidationContext validationContext)
{
// ObjectType はこの属性を付与したプロパティが属する
// クラス、即ち上のコードの Record クラスとなる
System.Reflection.PropertyInfo propertyInfo =
validationContext.ObjectType.
GetProperty(this._startTimePropertyName);
object propertyValue =
propertyInfo.
GetValue(validationContext.ObjectInstance, null);
if ((TimeSpan)value > (TimeSpan)propertyValue)
{
return ValidationResult.Success;
}
else
{
// ValidationResult の引数に null または "" を設定する
// と FormatErrorMessage メソッドの戻り値が設定される
return new ValidationResult(null);
}
}
}
}
上の画像・コードではクラスのコレクションのモデルバインディングを行っていますが、そのような場合でもリフレクションを使って比較対象のクラスのプロパティを特定して検証可能です。ただ、リフレクションを使うということで、性能的にどうかという懸念は消せてません。
というわけで後で考えて気が付いたのですが、上ような例の場合はリフレクションを使わなくても、IsValid メソッドの中のコードを以下のようにして可能でした。
var model = validationContext.ObjectInstance as Record;
if (model == null)
{
throw new NullReferenceException();
}
if ((TimeSpan)value > model.Open)
{
return ValidationResult.Success;
}
else
{
return new ValidationResult(null);
}
また、上のサンプルコードはサーバー側だけの検証で、クライアント側での JavaScript / jQuery による検証はかかりません。
単一のプロパティのカスタム検証であれば、@IT の記事の「第4回 検証属性の自作とクラス・レベルのモデル検証」のセクションに書かれているようにして、サーバー側とクライアント側両方の検証機能を実装できます。
しかし、今回のケースのように 2 つのプロパティにまたがった検証をする場合、スクリプトを ASP.NET MVC の検証システムの中に無理なく不整合なく取り込む具体例は少なくとも自分は見たことがないです。(自分が知らないだけだという可能性は否定しきれませんが)