LoginSignup
0
1

More than 5 years have passed since last update.

ActionMethodSelectorAttribute.IsValidForRequest メソッド内で ValueProvider にアクセスすると不整合が起きる

Posted at

はじめに

ASP.NET MVC で 1 つのフォームに複数ボタンを配置しそれぞれ別のアクションメソッドを呼び出したいというときには ActionMethodSelectorAttribute クラスを継承して以下の様なクラスを作成します。

ActionMethodAttribute.cs
public class ActionMethodAttribute : ActionMethodSelectorAttribute
{
    public ActionMethodAttribute(string name) => Name = name;

    public string Name { get; set; }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
        => controllerContext.Controller.ValueProvider.GetValue(Name) != null;
}

ボタンの name 属性値と ActionMethodAttribute の引数を一致させることでアクションメソッドを紐づける。

Index.cshtml
@using (Html.BeginForm())
{
    <button type="button" name="Button1">Button1</button>
    <button type="button" name="Button2">Button2</button>
}
ExampleController.cs
public class ExampleController : Controller
{
    [HttpGet]
    public ActionResult Index()
    {
        return View();
    }

    [ActionMethod("Button1")]
    [HttpPost]
    public ActionResult Button1()
    {
        return View(nameof(Index));
    }

    [ActionMethod("Button2")]
    [HttpPost]
    public ActionResult Button2()
    {
        return View(nameof(Index));
    }
}

ActionMethodAttribute の問題

通常に扱うぶんには問題ないのだが、上記コードでは特定条件下で非常に分かりづらいバグ?が発生する。
例えば https://localhost/users/100 のようにユーザー ID をパラメーターにアクセスし、更新と削除のアクションを行える画面のコントローラーを定義する。
Update アクションと、Delete アクションにはさきほどの ActionMethodAttribute を付与する。

UsersController.cs
public class UsersController : Controller
{
    [HttpGet]
    [Route("users/{id:int}")]
    public ActionResult Index(int id) {  }

    [ActionMethod("Update")]
    [HttpPost]
    [Route("users/{id:int}")]
    public ActionResult Update(int id) {  }

    [ActionMethod("Delete")]
    [HttpPost]
    [Route("users/{id:int}")]
    public ActionResult Delete(int id) { }
}

この状態で https://localhost/users/100 へアクセスすると UsersController.Index は int 型の id というパラメーターを受け取るシグネチャなのに null が渡されたというような例外が発生する。URL で id を指定しているのにどういうことなのか。。。
ここで重要なのは属性の並び順であり、ActionMethodAttribute と HttpPostAttribute の並び順を逆にするとひとまず正常に動作するようになる。

UsersController.cs
public class UsersController : Controller
{
    [HttpGet]
    [Route("users/{id:int}")]
    public ActionResult Index(int id) {  }

    [HttpPost]
    [ActionMethod("Update")]
    [Route("users/{id:int}")]
    public ActionResult Update(int id) {  }

    [HttpPost]
    [ActionMethod("Delete")]
    [Route("users/{id:int}")]
    public ActionResult Delete(int id) { }
}

属性は並び順に処理が実行されるのだが、なぜ HttpPostAttribute より先に ActionMethodAttribute が評価されるとエラーとなるのか。。。。
問題は恐らく context.Controller.ValueProvider.GetValue(Name) の箇所で ValueProvider にアクセスしているせいかと思われる。
MVC のソースを確認してみると以下のような流れで ValueProvider が作成される。
ControllerBase.ValueProvider プロパティに初回アクセス -> ValueProviderFactories.Factories -> ValueProviderFactoryCollection.GetValueProvider
ここで一度 ValueProvider が生成されてしまうと Index アクションへのバインディング時に値がなくなっているように見受けられる。
正確な原因は不明だが ValueProvider にアクセスすると発生するのは間違いないようである。
対処法としては上記にも挙げたように他の ActionMethodSelector より前に適用することである。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1