by WebSurfer
25. February 2020 16:09
ASP.NET MVC アプリケーションでモデルのプロパティにまたがるユーザー入力の検証に用いる CustomValidation 属性の実装方法を備忘録として書いておきます。

個々のプロパティのユーザー入力の検証には、下のサンプルコードの例のように Required, StringLength, Range などのデータアノテーション属性を利用するのが便利です。
加えて、上の画像のように苗字が佐藤の場合は年齢が 20 歳以上でなければならないという条件(例としてあまり適切ではないかもしれませんが)を付けたい場合はどうするかという話です。
その目的に使えるのが CustomValidation 属性です。プロパティにまたがる検証を行い、検証結果 NG の場合は新しい ModelState ディクショナリにエラーを追加して、既存の ModelState ディクショナリにマージするという操作を行います。(他に IValidatableObject を継承したカスタムモデルを使う方法もあります。それについては別の記事「IValidatableObject を継承したカスタムモデル」を見てください)
CustomValidation 属性には、パラメータとしてデータ型とメソッド名を指定します。データ型はこの属性を付与するモデルの型とします。メソッドは下のサンプルコードのシグネチャを持つ public static メソッドとします。
CustomVaidation 属性をモデルに付与する場合とプロパティに付与する場合とではコードが異なりますので注意してください。下のサンプルコードで、前者がモデルに付与する場合で、後者かプロパティに付与する場合です。
// 下の using 句は ASP.NET Core の場合。.NET Framework
// の MVC5 の場合は若干異なるので注意。namespace 以下
// のコードは Core / .NET Framework 同じになる。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
namespace MvcCoreApp.Models
{
// モデルレベル
[CustomValidation(typeof(CustomValidationModel2),
"ValidateNameAndAge")]
public class CustomValidationModel2
{
public int PersonId { set; get; }
[Display(Name = "名前")]
[Required(ErrorMessage = "{0} は必須入力")]
[StringLength(20, ErrorMessage =
"{0} は {2} から {1} 文字の範囲", MinimumLength = 2)]
public string Name { set; get; }
[Display(Name = "メールアドレス")]
[Required(ErrorMessage = "{0} は必須入力")]
[EmailAddress(ErrorMessage =
"有効な Email 形式ではありません")]
public string Mail { set; get; }
[Display(Name = "年齢")]
[Required(ErrorMessage = "{0} は必須入力")]
[Range(0, 200, ErrorMessage =
"{0} は {1} から {2} の範囲")]
public int Age { set; get; }
// 以下のシグネチャを持つ public static メソッド:
// public static ValidationResult ValidateNameAndAge(
// CustomValidationModel2 model)
// または
public static ValidationResult ValidateNameAndAge(
CustomValidationModel2 model,
ValidationContext context)
{
if (model == null)
{
throw new NullReferenceException();
}
if (model.Name.StartsWith("佐藤") && model.Age < 20)
{
return new ValidationResult(
"佐藤さんは二十歳以上でなければなりません");
}
else
{
return ValidationResult.Success;
}
}
}
// プロパティレベル
public class CustomValidationModel3
{
public int PersonId { set; get; }
[Display(Name = "名前")]
[Required(ErrorMessage = "{0} は必須入力")]
[StringLength(20, ErrorMessage =
"{0} は {2} から {1} 文字の範囲", MinimumLength = 2)]
public string Name { set; get; }
[Display(Name = "メールアドレス")]
[Required(ErrorMessage = "{0} は必須入力")]
[EmailAddress(ErrorMessage =
"有効な Email 形式ではありません")]
public string Mail { set; get; }
[Display(Name = "年齢")]
[Required(ErrorMessage = "{0} は必須入力")]
[Range(0, 200, ErrorMessage =
"{0} は {1} から {2} の範囲")]
[CustomValidation(typeof(CustomValidationModel3),
"ValidateNameAndAge")]
public int Age { set; get; }
// 引数 age には年齢が int 型の値として代入される
public static ValidationResult ValidateNameAndAge(int age,
ValidationContext context)
{
var model =
context.ObjectInstance as CustomValidationModel3;
if (model == null)
{
throw new NullReferenceException();
}
if (model.Name.StartsWith("佐藤") && age < 20)
{
return new ValidationResult(
"佐藤さんは二十歳以上でなければなりません");
}
else
{
return ValidationResult.Success;
}
}
}
}
この記事の画像はプロパティレベルの場合で、エラーメッセージもプロパティレベルで表示されています。モデルレベルの場合はエラーメッセージはサマリーに表示されます。