tekitoumemo’s diary

思ったことを書くだけ。長文版Twitter

ASP.NET MVCにおけるモデル検証について

https://4.bp.blogspot.com/-NZWVQX1HeY0/W4tss_UVZmI/AAAAAAABOQ0/xBD5M6o3HAs82NfznQPvkgoxCHtO1RDHQCLcBGAs/s450/fashion_model_woman.png

かなり一般的なネタですが、以外に実装されている現場が少なく、色々処理した後にエラーチェックしてバグる現象が発生していることが多い気がするので備忘録として。以下に書いているコードは.Net Core2.1です。Frameworkでもほぼ一緒なので参考になるかなと思います。

モデル検証って言うとなんか小難しく感じるのですが、要はエラーチェックです。公式はこう言ってます。

アプリでは、データベースにデータを格納する前に、データを検証する必要があります。 データにセキュリティ上の脅威がないかどうかを確認し、種類とサイズが適切に設定されていることを検証しなければなりません。また、ご自身のルールに準拠している必要もあります。

で、ASP.NETだと簡単にできるよって言ってます。

検証を実装するのは冗長で面倒な場合がありますが、必要不可欠です。 MVC では、検証はクライアントとサーバーの両方で発生します。
さいわい、.NET では検証が検証属性に抽象化されています。 これらの属性には検証コードが含まれているため、開発者が記述しなければならないコードの量は少なくて済みます。

まぁ、他のフレームワークでもあるよねって感じですが。

本題に入ります。

モデル

Personというモデルがあり、名前と年齢が定義されています。

public class Person {

    public string Name { get; set; }

    public int Age { get; set; }

}

名前は10文字まで、年齢は99歳までというバリデーションをつける場合はAttributeをつけます。

[Display (Name = "名前")]
[MaxLength (10)]
public string Name { get; set; }

[Display (Name = "年齢")]
[Range (0, 99)]
public int Age { get; set; }

これで条件通りのバリデーションをつけれますが以下のようなエラーが出力されます。

"The field 名前 must be a string or array type with a maximum length of '10'."

これだとアプリ毎に適切なメッセージじゃないのでメッセージをつけます。

[Display (Name = "名前")]
[MaxLength (10, ErrorMessage = "{0}は10文字まで")]
public string Name { get; set; }

[Display (Name = "年齢")]
[Range (0, 99, ErrorMessage = "{0}は99歳まで")]
public int Age { get; set; }

これで以下のようなメッセージが出力されます。

"年齢は99歳まで"
"名前は10文字まで"

バリデーションはいろんな種類があるので以下を参考にしてください。
docs.microsoft.com

次にアプリ独自の検証は必ず必要なので、CustomValidationAttributeで検証します。今回はsaitoって名前で29歳(僕)だったらエラーにします。

public class Person {

    [Display (Name = "名前")]
    [CustomValidation (typeof (Person), "CheckName")]
    public string Name { get; set; }

    [Display (Name = "年齢")]
    public int Age { get; set; }

    public static ValidationResult CheckName (string value, ValidationContext context) {
        var model = (Person) context.ObjectInstance;
        if (model.Name == "saito" && model.Age == 29) {
            return new ValidationResult ("この人はダメです。");
        }
        return ValidationResult.Success;
    }
}

上記のコードでモデルを取得して組み合わせ技で検証出来るのでだいたいは事足りるかなと思います。

コントローラ

次にコントローラー側の検証します。

WEBAPIなどに使われるApiControllerAttributeを指定している場合はコントローラ側に来る前に以下のエラーを吐きました。ちょっと独自のエラーを返したい場合はどうするのかよくわかりません。いつか調べます(絶対調べない)

{
    "Age": [
        "年齢は99歳まで"
    ],
    "Name": [
        "名前は10文字まで"
    ]
}

MVCの場合は以下のように検証します。ModelState.IsValidはモデルにエラーがあるか判定するフラグでfalseだったらエラーがあるってことです。

[HttpPost]
public string Post ([FromBody] Person person) {

    if (!ModelState.IsValid) {
        // ここでエラーチェック
    }
    return "Success";
}

独自のエラーモデルなどを使っている場合はModelStateからエラーメッセージとプロパティを取得できます。

// プロパティ毎のエラーを保持
ModelState.Values
// 一番最初のプロパティのエラーを保持
ModelState.Values.First ().Errors
// 一番最初のプロパティのエラーの最初のメッセージ(エラーが複数ある場合)
ModelState.Values.First ().Errors.First ().ErrorMessage
// エラーがあるプロパティの名前
ModelState.Keys

いろいろ書きましたが、これだけ覚えていればだいたいのエラーチェックがAttributeで済むと思います。エラーチェックは作り込んでから直すと結構大変なんでここら辺は習得しておく必要があるかなと思います。いつものごとく雑ですが、githubにあげましたので参考になればと思います。個人の趣味ブログなのですげーテキトーにコード書いてますのでご注意を。
github.com