xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

ASP.NET Core MVC における Required 属性と BindRequired 属性の統合

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; }
}