しばやん雑記

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

ASP.NET Core 2.2 で追加された Endpoint Routing と Core MVC での互換性

いろんな場所でちょいちょい話してた Endpoint Routing について調べたので書きます。

パフォーマンス改善が目立った ASP.NET Core 2.2 の更新内容としては、唯一 Endpoint Routing は特定のパターンでは Breaking change となります。

そもそも Endpoint Routing とは、という点に触れた記事などがあまりないのですが、以下の記事は最低限読んでおいた方が良いでしょう。ちなみに昔は Dispatcher とも呼ばれていました。

ASP.NET Core のルーティングは歴史的経緯から、ASP.NET MVC の実装をほぼそのまま持ってきたような形になっていたようです。ざっくり説明すると、そもそもルーティングというのは MVC に限った機能ではないので、2.2 で Endpoint Routing という形で別にしたという話です。

とはいえ開発者が直接触ることはほとんどなく、フレームワークが使うものという認識で良さそうです。

Endpoint Routing は CompatibilityVersion.Version_2_2 を指定するとデフォルトで有効化されるので、互換性を考慮して Endpoint Routing だけ無効化したいという場合には、明示的にオフにすることも出来ます。

public void ConfigureServices(IServiceCollection services)
{
    // Endpoint Routing のみ明示的にオフにする
    services.AddMvc(options => options.EnableEndpointRouting = false)
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

実装に興味がある人は MvcApplicationBuilderExtensions を覗いてみると良いと思います。

Endpoint Routing の実装は Routing の一機能として Microsoft.AspNetCore.Routing に含まれています。これまでのルーティングよりも拡張性が高く、パフォーマンスも優れているので特に問題なければ移行をお勧めしています。

ここからは互換性に影響する Endpoint Routing での挙動を実際に確認したのでまとめました。

存在しないアクションのリンクを生成しない

規約ベースのルーティングを使っている場合には、以下のように存在しないコントローラとアクションに対してのリンクを 2.1 までは生成していました。

存在しないので ReSharper が赤線を引いて怒り狂っています。

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

このコードは CompatibilityVersion が 2.1 になっている、もしくは Endpoint Routing が無効化されている場合には、以下のように存在しないアクションへのリンクを生成します。

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

当然ながらアクセスしても 404 になります。存在しないアクションなので正しいです。

そして Endpoint Routing が有効になっている場合には、リンク自体の生成が行われません。実際には空文字列が返されるように挙動が変更されました。

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

かなりのレアケースだとは思いますが、別 MVC アプリケーションへのリンクを生成するために流用していた場合などでは、バージョンアップで動作が変わることになります。

属性ルーティングにおけるルートパラメータの無効化

アクションの存在チェックが入ったことよりも、個人的にはこっちの方が影響範囲が広かったです。ドキュメントは分かりやすいような、分かりにくい説明になっていたので実際にコードを出して説明します。

例えば、属性ルーティングを使って以下のようなコントローラとアクションを用意します。よくある id をルートパラメータから取るアクションです。

[Route("[controller]")]
public class ProductController : Controller
{
    [HttpGet("[action]/{productId}")]
    public IActionResult Details(int productId)
    {
        return View();
    }

    [HttpGet("[action]/{productId}")]
    public IActionResult Edit(int productId)
    {
        return View();
    }

    [HttpGet("[action]/{productId}")]
    public IActionResult Delete(int productId)
    {
        return View();
    }
}

ここで Details ページから Edit や Delete が行えるリンクを用意しているのは、結構あるパターンだと思います。実際にスキャフォールディングでビューを生成したらそういうコードになっているはずです。

サンプルとして以下のような Details ビューを用意しました。単純にリンクを生成しているだけですが、アクション名だけを指定しているのが特徴です。

<h4>Tag Helper</h4>

<p>
  <a asp-action="Details">Details</a>
  &nbsp;|&nbsp;
  <a asp-action="Edit">Edit</a>
  &nbsp;|&nbsp;
  <a asp-action="Delete">Delete</a>
</p>

<h4>Url Helper</h4>

<ul>
  <li>@Url.Action("Details")</li>
  <li>@Url.Action("Edit")</li>
  <li>@Url.Action("Delete")</li>
</ul>

Url Helper を載せているのは分かりやすくするためで、Tag Helper と挙動が違うというわけではないです。

このコードを Endpoint Routing を無効にした状態で実行すると、表示しているページのルートパラメータを引き継いだ形で、Edit と Delete のリンクが生成されていることが確認できます。

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

実行結果からわかるように ASP.NET Core MVC 2.1 までの属性ルーティングでは、新しく生成するリンクに対しても現在のルートパラメータを使うという挙動になっていました。ルートパラメータを引き回したいケースは結構多いので、これまでは再利用される前提のコードを結構書いていました。

そして Endpoint Routing を有効にして実行すると、Details 以外はリンクが生成されなくなります。

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

Endpoint Routing ではルートパラメータの再利用が同じコントローラとアクションに対してのみ行われるように変更されたので、上の例のように別アクションへのリンクを生成する場合には、明示的にパラメータを渡す必要があります。GET / POST で同じアクションの場合はこれまで通りです。

ちなみに規約ベースでは元々ルートパラメータの再利用が行われないので、今回属性ルーティングに対しても挙動を合わせたというのが正確な説明になります。