しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET MVC 5 で追加された属性ベースのルーティングを使ってモデルバインダ絡みのエラーを回避する

ASP.NET MVC を利用した開発を行っている人は、下のエラー画面を 1 度は見たことがあるかと思います。

f:id:shiba-yan:20140826173411p:plain

MVC では URL ルーティングで定義されたパラメータやクエリ文字列を、モデルバインダが適切な型へ変換してアクションの引数へバインドする仕組みになっています。この時にアクションの引数の型が int なのに null が渡されたり、アルファベットなどが指定された場合は変換に失敗しエラーとなります。

具体的に以下のアクションを例に見ていきます。

public class ArticleController : Controller
{
    public ActionResult Details(int id)
    {
        return View();
    }
}

この場合 Details アクションは int 型の id という名前の引数を取るので、アクセスする URL としては /article/details/123 という形式である必要があります。

しかし /article/details/ や /article/details/abc といったような URL でアクセスされた場合には int への変換が不可能なので、モデルバインダから例外が投げられ HTTP エラーになるという仕組みです。URL ルーティングの定義を変更し、id というパラメータが数字のみ受け付けるように正規表現で制約をかけることも出来ますが、アクションが多い場合には非常に手間がかかります。

routes.MapRoute(
    "Article",
    "article/details/{id}",
    new { controller = "Article", action = "Index" },
    new { id = @"^[0-9]+$"}
);

このようなルーティング定義をアクションごとに手動で定義していくのはとても大変ですし、間違いも多くなってしまいそうですね。しかしアクション側としては必要な型やフォーマットが決まっているので、不正な URL の場合には 404 として扱ってほしいです。

なので、もっと楽に盤石な URL ルーティングを定義するためにも、MVC 5 から追加された属性ベースのルーティングを使って定義していきたいと思います。既に MSDN ブログでとても詳細な使い方が紹介されているので、まずはこの記事を一通り読んでおいてください。

Attribute Routing in ASP.NET MVC 5 - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

割と今更感のある MVC 5 ネタですが、仕事でこのエラーがちょいちょい発生してとてもイラついたのでブログを書くことにしました。

属性ベースのルーティングは標準の URL ルーティングよりも引数へ簡単に制約をつけることが出来ます。以下の例で使っているように {articleId:int} と書くと int しか受け付けなくなります。

[RoutePrefix("article")]
public class ArticleController : Controller
{
    [Route("{articleId:int}")]
    public ActionResult Details(int articleId)
    {
        return View();
    }

    [Route]
    public ActionResult Add()
    {
        return View();
    }

    [Route("{articleId:int}/edit")]
    public ActionResult Edit(int articleId)
    {
        return View();
    }

    [HttpPost]
    [Route("{articleId:int}/edit")]
    public ActionResult Edit(int articleId, FormCollection collection)
    {
        return View();
    }
}

注意点としては、出来るだけ id といった同じ名前のパラメータを使いまわさないことと、GET と POST で同じアクション名のメソッドを定義している場合には、両方ともに Route 属性を付ける必要がある点です。属性ベースのルーティングでは、内部的に ActionDescriptor を保持しているので、同じ URL テンプレートだからと言って使い回しが効くわけではありません。

このルーティング定義で以下のような URL が有効になります。そして、それ以外の URL でアクセスされた場合には 404 になります。

  • /article/123
  • /article/add
  • /article/123/edit

試しに int が必要なパラメータをアルファベットに変更してアクセスしてみます。

f:id:shiba-yan:20140826224459p:plain

アプリケーションエラーとならずに、意図したとおり 404 として正常に処理が完了しました。

先ほどの例では、全てのアクションに Route 属性を付けていましたが、特別な URL テンプレートの指定が必要ない場合にはコントローラレベルで属性を付けることで省略が可能です。

[Route("{action}"), RoutePrefix("admin")]
public class AdminController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Settings()
    {
        return View();
    }
}

属性ベースのルーティングは自動的に URL ルーティングを組み立てているだけなので、Route 属性の URL テンプレートを空にしてしまうとアクション全てへアクセスできなくなるので注意。