miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET MVC の UpdateModel と ValidateModel に思いを馳せる

ASP.NET MVC の Contoller クラスには、UpdateModel(...) と ValidateModel(...) のヘルパーメソッドが定義されています。
このメソッドを使うことで、モデルのバインド先の選択、検証対象の選択を行うことができます。

これらの関連メソッドは、Controller クラスにて以下のように定義されています(一部を掲載)

protected internal void UpdateModel<TModel>(TModel model) where TModel : class;
protected internal void ValidateModel(object model);
protected internal bool TryUpdateModel<TModel>(TModel model) where TModel : class;
protected internal bool TryValidateModel(object model);

この2つについて、調べてみました。

UpdateModel とは

UpdateModel は、入力値でモデルを更新し、検証し、ModelState に情報を追加します。

試しに、以下のように Student.cs と、ビュー、コントローラーを用意します。

Student.cs :
FirstName, LastName どちらにも[Required]属性を付与します。

using System.ComponentModel.DataAnnotations;

public class Student
{
    [Required]
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }
}

Create.cshtml : (ただの入力フォーム)

@using (Html.BeginForm())
{
    <div class="form-horizontal">
        <h4>Student</h4>
        <hr />
        @Html.ValidationSummary(
            excludePropertyErrors: false,
            message: "",
            htmlAttributes: new { @class = "text-danger" })

        FisrstName : @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "form-control" } })
        LastName : @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-control" } })
        <input type="submit" value="Create" class="btn btn-default" />
    </div>
}

StudentController.cs :

[HttpPost]
[ActionName("Create")]
public ActionResult CreatePost()
{
    //Body:"FirstName=Sato&LastName="でPOSTした場合

    var student = new Student() { FirstName = "DefaultFirst", LastName = "DefaultLast" };
    TryUpdateModel(student);

    Debug.WriteLine(student.FirstName); //sato
    Debug.WriteLine(student.LastName);//空
    OutputModelErrors();//LastName フィールドが必要です。

    return View();
}

void OutputModelErrors()
{
    foreach(var modelState in ModelState)
    {
        foreach (var error in modelState.Value.Errors)
        {
            Debug.WriteLine(error.ErrorMessage);
        }
    }
}

StudentController.cs では、Student クラスの各プロパティに適当な値をいれた変数を用意し、
TryUpdateModel メソッドを呼び出しています。

f:id:miso_soup3:20160328222128p:plain

画像のように入力しPOSTした場合(Body:"FirstName=Sato&LastName="でPOSTした場合)、StudentController.cs では、student変数に入力値がバインドされます。LastName は[Required]属性が付与されてますが、入力値は空で送信されているので、
「LastName フィールドが必要です。」という検証エラーの情報が ModelState に追加されます。
student.LastName に空の値がバインドされることに注意です。

Try の有無の違い

TryUpdateModel と UpdateModel の違いは、検証エラーがある場合に、例外を投げるかどうかの違いです。
例えば先ほどの StudentController.cs の TryUpdateModel() を UpdateModel() に変更して、同じように POST してみると下のように例外が発生します。

f:id:miso_soup3:20160328222146p:plain

UpdateModel() は、下の記事でも登場するように、使用頻度が高いメソッドです。

この他、プロパティのホワイトリスト/ブラックリスト、IValudProvider、値のKeyのPrefix を指定することができます。

protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class;

推測ですが特に理由がない限り、UpdateModel() を使うのではなく、Controller のメソッドの引数にモデルを定義する方法が一般的に使われてるかなと思います。
(特にDBから取得してきたような監視のあるEntityに対してUpdateModel() を使う際は、ホワイトリストの使用が必須になるなど、動作を把握し注意が必要です。)

(また補足として、「入力値で値をバインドする」と言いましたが、この入力値は ValueProvider から提供されている値なので、手を加えれば Cookie や Session の値を対象とすることもできます)

ValidateModel とは

入力値をバインドする UpdateModel() とは違い、ValidateModel() はバインドを行いません。
インスタンスの値に対して検証を行い、ModelState に情報を追加します。

先ほどとやや同じ条件で試してみます。
今度は、FirstName, LastName どちらも値を入力して送信します。

f:id:miso_soup3:20160328222154p:plain

StudentController.cs にて、TryValidateModel() を呼び出します。

[HttpPost]
[ActionName("Create")]
public ActionResult CreatePost()
{
    //Body:"FirstName=Taro&LastName=Sato"でPOSTした場合

    var student = new Student() { FirstName = "DefaultFirst", LastName = "" };
    TryValidateModel(student);

    Debug.WriteLine(student.FirstName); //DefaultFirst
    Debug.WriteLine(student.LastName);//空
    OutputModelErrors();//LastName フィールドが必要です。

    return View();
}

このとき、student 変数には入力値がバインドされず、初期化時のプロパティの値(DefaultFirstと"")のまま格納されています。
また、フォームにてFirstName, LastName がどちらも入力されて送信されていますが、バインドは行われていないので、student.LastName には空文字ということで、
ModelState には「LastName フィールドが必要です。」というエラー情報が格納されます。

TryValidateModel と ValidateModel の違いは、UpdateModel のときと同様で、検証エラーがある場合に、例外を投げるかどうかの違いです。

ValidateModel の注意

後者で説明した ValidateModel では気を付ける点があります。それは、子プロパティのモデルに対して検証が走らないということです。

ここでは、次のように Cource クラスのリストをプロパティとして定義し、Title プロパティに [Required] を付与して試してみました。

public class Student
{
    [Required]
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }

    public IList<Cource> CourceList { get; set; }
}

public class Cource
{
    [Required]
    public string Title { get; set; }
}

StudentController.cs にて、Title を空にした student を用意しました。
ValidateModel() では、下のように各 Cource のインスタンスに対して ValidateModel() を呼び出さないと、検証が走りません。

[HttpPost]
[ActionName("Create")]
public ActionResult CreatePost()
{
    var student = new Student() { FirstName = "DefaultFirst", LastName = "DefaultLast" };
    student.CourceList = new List<Cource>()
    {
        new Cource() { Title = "" },
        new Cource() { Title = "" }
    };

    TryValidateModel(student);
    foreach(Cource cource in student.CourceList)
    {
        TryValidateModel(cource);
    }

    Debug.WriteLine(student.FirstName); //DefaultFirst
    Debug.WriteLine(student.LastName);//DefaultLast
    OutputModelErrors();//Title フィールドが必要です。

    return View();
}

ちなみにこの場合、Cource は2つ持っていますが、ModelState には、"Title" というキーで「Title フィールドが必要です。」というエラーが1つのみ格納されます。
もし、このエラーを1つ1つ識別する場合(2つのエラーとしたい場合)は、TryValidateModel メソッドで prefix を指定します。

TryValidateModel(student);
int courceIndex = 0;
foreach(Cource cource in student.CourceList)
{
    TryValidateModel(cource, prefix: (++courceIndex).ToString());
}

そうすると、下のように異なるキーで ModelState にエラー情報を追加することができます。

f:id:miso_soup3:20160328222211p:plain

この prefix の指定は UpdateModel() の方でもできるのですが用途が違います。
UpdateModel() の方は入力値を検索するときに、指定された prefix がついている入力値を検索します。
ValidateModel() の方は、ModelState にエラー情報を格納するときに、キーに指定した prefix を付けて追加します。
この違いがややこしい感じ。

UpdateModel() を使う例、条件によって検証を変える

UpdateModel()やValidateModel()は、動作を把握しておけばいざというときに役に立つヘルパーかもしれません。

私の場合、条件分岐によって検証ロジックを変えたいときに UpdateModel() を使いました。
Aパターンのときには、FirstName プロパティを必須にしたいけど、Bパターンのときは、FirstName は文字数だけ検証したい、けど、そのロジックは検証属性で表現したい、といったことです。

パターン毎にモデルを定義します。Student_A は必須&文字数検証、Student_B は文字数検証のみ。

public class Student_A
{
    [Required]
    [MaxLength(10)]
    public string FirstName { get; set; }
    [Required]
    [MaxLength(10)]
    public string LastName { get; set; }
}

public class Student_B
{
    [MaxLength(10)]
    public string FirstName { get; set; }
    [MaxLength(10)]
    public string LastName { get; set; }
}

StudentController.cs にて、条件分岐はここでは簡単に bool 変数で定義しました。
flag が true の時は、Student_A の属性の検証が行われ、
flag が false の時は、Student_B の属性の検証が行われます。

[HttpPost]
[ActionName("Create")]
public ActionResult CreatePost()
{
    bool flag = true;
    Type userModelType = userModelDic[flag];
    dynamic model = Activator.CreateInstance(userModelType);
    TryUpdateModel(model);
    
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    return View();
}

IDictionary<bool, Type> userModelDic = new Dictionary<bool, Type>()
{
    { true, typeof(Student_A) },
    { false, typeof(Student_B) }
};

UpdateModel() と ValidateModel() のコードです↓
https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Controller.cs#L648

ASP.NET Web API の ApiController.cs にはありませんでした。

ASP.NET Core 1.0 では同様のメソッドが用意される予定です。
https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs#L1113

まとめ

★UpdateModel と ValidateModel の違い:

  • UpdateModel()
    • 入力値の値でモデルを更新し、検証し、ModelState に情報を追加する。
  • ValidateModel()
    • インスタンスの値を検証し、ModelState に情報を登録する。

★Try の有無の違い:

Try がない場合は検証エラーとなった場合は例外が発生します。Try がつく場合は、例外は発生せず検証エラーの有無を返します(ModelState.IsValidを返します)。

★prefix の扱い:

UpdateModel() の方は入力値を検索するときに、指定された prefix がついている入力値を検索します。
ValidateModel() の方は、ModelState にエラー情報を格納するときに、キーに指定した prefix を付けて追加します。

★注意:
ValidateModel は階層化のモデルに対して検証を行わない。


UpdateModel() と ValidateModel() は、ASP.NET MVC 2 ?のころからあるみたいです。
成熟前から用意されていて、次期の ASP.NET MVC Core 1.0 にも用意されていて…
Controller クラスにあるヘルパーメソッドの中でもちょっと用途が異なっていて…
というか UpdateModel と ValidateModel 処理的に全然違う…
サンプルでは使用されるけど、実際には、工夫のある使い方で使用されたりして…
でも ASP.NET MVC のモデルバインドの本質を考えると、引数にバインドするだけじゃなくて、Controller にてバインドヘルパーが用意されているのは納得…

と、いろいろ思ったのでした。