ASP.NET Core MVC に限らず ASP.NET MVC 時代からそうですが、最もよく利用するモデル検証属性として Required
属性があります。Required
の名前の通り「入力必須」であることを表すのですが、実際の挙動は非 null の判定を行うものです。
ですので、以下のように int
のような値型に対して Required
属性を付与しても効果はありません。これは既定値 0 が設定されるためです。
public class Person { [Required] // null 以外を強制できる public string Name { get; set; } [Required] // これは特に意味がない public int Age { get; set; } }
public IActionResult Post(Person person) { // Age に値が設定されていなくても検証は valid になる if (!this.Model.IsValid) return this.BadRequest(); // do something }
もはや NotNull
属性の方がネーミングとして分かりやすいんじゃないかと思うくらいです。
ASP.NET MVC 時代の解決方法
これの値型の既定値が入ってしまって入力されているのかが分からない問題を解決するため、ASP.NET MVC 時代には Nullable 型
を利用することで回避してきました。
public class Person { [Required] public string Name { get; set; } [Required] // int? なら null 以外かどうかで判断できる public int? Age { get; set; } }
ちっぽけな問題かもしれませんが、int
として扱いたいのに int?
を強要されるのはあまり好ましいことではありません。
ASP.NET Core MVC 時代の解決方法
今知りたいのは null
かどうかではなく、値がバインドされたかどうかです。値型に対して値がバインドされたかどうかを判定するための機能として、ASP.NET Core MVC から BindRequired
属性が追加されました。以下のようにすれば、望んだ通り入力必須の検証を行うことができます。これは嬉しい改善です。
public class Person { [Required] public string Name { get; set; } [BindRequired] // 値がバインドされることを強制 public int Age { get; set; } }
Required
属性と BindRequired
属性を統合する
BindRequired
属性が追加されて願いは叶ったのですが、参照型と値型で 2 種類の属性を使い分けるのはミスも出やすいですし何より分かりにくいです。そこで、以下のようにバインディングをカスタマイズしてしまうことで [Required]
属性に対してもバインディングを必須にしてみます。
public class BindingRequiredMetadataProvider : IBindingMetadataProvider { public void CreateBindingMetadata(BindingMetadataProviderContext context) { // パフォーマンスを最大化するため敢えて NO LINQ for (var i = 0; i < context.Attributes.Count; i++) { // [Required] が付いていたらモデルバインドを必須にしちゃう if (context.Attributes[i] is RequiredAttribute) { context.BindingMetadata.IsBindingRequired = true; return; } } } }
機能を有効に利用する場合ば Startup.cs
の中で以下のように設定します。
services.AddMvc(o =>
{
o.ModelMetadataDetailsProviders.Add(new BindingRequiredMetadataProvider());
});
こうすることで、冒頭のように [Required]
属性だけで入力必須を表現することができるようになります。便利!
public class Person { [Required] public string Name { get; set; } [Required] // これで入力必須にできる public int Age { get; set; } }