これを読んでできること
- 2つの入力項目を検証し、定義した大小関係を満たしているか検証できる
- カスタムバリデーションを作れるようになる
ValidationAttributeとは
すべての検証属性の基本クラスとして機能します。
ValidationAttribute クラス - docs.microsoft.com
MSのドキュメントを引用しましたが、これだけではよくわかりませんね。
ValidationAttribute とはフォームからの入力値やモデルオブジェクトのプロパティが正しいかどうかの検証を強制し、また正しさを定義する属性の基底クラスです。
カスタム検証属性を定義する場合はこれを継承していきます。
参考によく使われる検証属性を以下に記載します。
| 属性名 | 機能 | 
|---|---|
| Required | 必須項目。nullや未入力の場合エラーとします | 
| StringLength(int) | 最大文字長。指定した文字数を超えるとエラーとします | 
| EmailAddress | メールアドレス形式の項目。メールアドレスの形式を満たさない場合エラーとします | 
System.ComponentModel.DataAnnotations には標準で複数の検証属性が実装されています。しかし、開始時刻と終了時刻のように他のプロパティに依存する検証属性はありません1。そのためカスタム検証属性を作る必要があります。
カスタム検証属性を作ってみた
結果として、検証属性を4種類作りました。
どれもほぼ内容が同じなので「AよりBが大きい」を検証する GreaterThanAttribute 2のソースコードを添付します。
[AttributeUsage(AttributeTargets.Property)]
    public class GreaterThanAttribute : ValidationAttribute
    {
        public string OtherProperty { get; private set; }
        public string OtherPropertyDisplayName { get; internal set; }
        public GreaterThanAttribute(string otherProperty)
        {
            OtherProperty = otherProperty;
            ErrorMessage = "{0}は{1}より大きい値を指定してください。";
        }
        public override string FormatErrorMessage(string name)
        {
            // エラーメッセージを返す
            return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, OtherPropertyDisplayName ?? OtherProperty);
        }
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // 比較対象のPropertyInfo
            PropertyInfo propertyInfo = validationContext.ObjectType.GetProperty(OtherProperty);
            // 比較対象のプロパティの値
            object propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
            Type type = propertyInfo.PropertyType;
            if (type == typeof(DateTime))
            {
                // ここで値の比較。条件を満たしていれば検証成功を返す
                if ((DateTime)value > (DateTime)propertyValue)
                {
                    return ValidationResult.Success;
                }
            }
            else if (type == typeof(int))
            {
                if ((int)value > (int)propertyValue)
                {
                    return ValidationResult.Success;
                }
            }
            // ...other type
            if (OtherPropertyDisplayName == null)
            {
                OtherPropertyDisplayName = GetDisplayNameForProperty(validationContext.ObjectType, OtherProperty);
            }
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }
        // 比較対象のプロパティ名を取得する。ここはオマケなので削っても問題ない
        private static string GetDisplayNameForProperty(Type containerType, string propertyName)
        {
            ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(containerType);
            PropertyDescriptor property = typeDescriptor.GetProperties().Find(propertyName, true);
            if (property == null)
            {
                throw new ArgumentException();
            }
            IEnumerable<Attribute> attributes = property.Attributes.Cast<Attribute>();
            DisplayAttribute display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
            if (display != null)
            {
                // DisplayAttributeがついてたらその名称を返す
                return display.GetName();
            }
            DisplayNameAttribute displayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
            if (displayName != null)
            {
                // DisplayNameAttributeがついてたらその名称を返す
                return displayName.DisplayName;
            }
            return propertyName;
        }
        private static ICustomTypeDescriptor GetTypeDescriptor(Type type)
        {
            return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
        }
    }
カスタム検証属性を使用するモデル側は以下の通りです。
        // 開始時刻
        // LessThanAttributeのコードは割愛
        [LessThan("EndDateTime")]
        public DateTime StartDateTime { get; set; }
        // 終了時刻
        [GreaterThan("StartDateTime")]
        public DateTime EndDateTime { get; set; }
使い方
開始時刻 < 終了時刻 であることを検証したい場合、 StartDateTime に [LessThan] 属性、 EndDateTime に [GreaterThan] 属性を付けます。引数には比較対象のプロパティを string で指定します。Compare 属性と似ていますね。
属性をつけることでASP.NET MVCのモデル検証やBlazorのフォーム検証時に大小関係が正しいか検証されます。大小関係を満たさない場合、「StartDateTimeはEndDateTimeより小さい値を指定してください。」や「EndDateTimeはStartDateTimeより大きい値を指定してください。」がエラーメッセージとして表示されます。
おわりに
もっと良い書き方があるような気もしますが、一旦ここで切り上げます。型の分岐とかもっとイケてる書き方3ができそうですね。元気があるときに書き直したいと思います。
参考
- CompareAttribute.cs(ベースとしたソース)