ASP.NET Core のルーティング

作成者: Ryan NowakKirk LarkinRick Anderson

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

ルーティングの役割は、受信した HTTP 要求を照合し、それらの要求をアプリの実行可能なエンドポイントにディスパッチすることです。 エンドポイントは、アプリの実行可能な要求処理コードの単位です。 エンドポイントはアプリで定義され、アプリの起動時に構成されます。 エンドポイントの照合プロセスでは、要求の URL から値を抽出し、それらの値を要求の処理に提供できます。 アプリからのルート情報を使用して、ルーティングでエンドポイントにマップする URL を生成することもできます。

アプリでは、次のものを使用してルーティングを構成できます。

この記事では、ASP.NET Core のルーティングについて詳しく説明します。 ルーティングの構成については、以下を参照してください。

ルーティングの基本

次のコードでは、ルーティングの基本的な例を示します。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前述の例には、MapGet メソッドを使用するエンドポイントが 1 つ含まれます。

  • HTTP GET 要求がルート URL / に送信された場合:
    • 要求デリゲートが実行されます。
    • Hello World! が HTTP 応答に書き込まれます。
  • 要求メソッドが GET ではない場合、またはルート URL が / ではない場合は、一致するルートはなく、HTTP 404 が返されます。

ルーティングでは、UseRoutingUseEndpoints によって登録されたミドルウェアのペアを使用します。

  • UseRouting では、ルートの照合がミドルウェア パイプラインに追加されます。 このミドルウェアによって、アプリで定義されているエンドポイントのセットが調べられ、要求に基づいて最適な一致が選択されます。
  • UseEndpoints では、エンドポイントの実行がミドルウェア パイプラインに追加されます。 選択されたエンドポイントに関連付けられているデリゲートが実行されます。

通常は、アプリで UseRouting または UseEndpoints を呼び出す必要はありません。 WebApplicationBuilder は、Program.cs に追加されたミドルウェアを UseRoutingUseEndpoints でラップするミドルウェア パイプラインを構成します。 ただし、アプリでは、これらのメソッドを明示的に呼び出すことによって、UseRoutingUseEndpoints の実行順序を変更できます。 たとえば、次のコードでは、UseRouting を明示的に呼び出しています。

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

上のコードでは以下の操作が行われます。

  • app.Use の呼び出しにより、パイプラインの開始時に実行されるカスタム ミドルウェアが登録されます。
  • UseRouting の呼び出しにより、ルートの照合ミドルウェアがカスタム ミドルウェアの "後に" 実行されるように構成されます。
  • MapGet に登録されたエンドポイントが、パイプラインの最後で実行されます。

前述の例に UseRouting の呼び出しが含まれていなかった場合、カスタム ミドルウェアは、ルートの照合ミドルウェアの "後に" 実行されます。

注: ルートは、パイプラインの "最後" で、WebApplication execute に直接追加されます。

エンドポイント

エンドポイントを定義するには、MapGet メソッドが使用されます。 エンドポイントとは、次のようなものです。

  • URL と HTTP メソッドを一致させることによって選択できます。
  • デリゲートを実行することによって実行できます。

アプリによって一致させて実行できるエンドポイントは、UseEndpoints で構成します。 たとえば、MapGetMapPost、および類似のメソッドでは、要求のデリゲートがルーティング システムに接続されます。 他のメソッドを使用して、ASP.NET Core フレームワークの機能をルーティング システムに接続できます。

次の例では、より高度なルート テンプレートによるルーティングを示します。

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

文字列 /hello/{name:alpha} は、ルート テンプレートです。 ルート テンプレートは、エンドポイントの一致方法を構成するために使用されます。 この場合、テンプレートは次のものと一致します。

  • /hello/Docs のような URL
  • /hello/ で始まり、その後に一連の英字が続く任意の URL パス。 :alpha では、英字のみと一致するルート制約が適用されます。 ルート制約については、この記事で後ほど説明します。

URL パスの 2 番目のセグメント {name:alpha} は次のようになります。

  • name パラメーターにバインドされます。
  • キャプチャされて HttpRequest.RouteValues に格納されます。

次の例では、正常性チェックと承認を使用するルーティングを示します。

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

上の例では、次の方法が示されています。

  • ルーティングで承認ミドルウェアを使用できます。
  • エンドポイントを使用して、承認動作を構成できます。

MapHealthChecks の呼び出しにより、正常性チェック エンドポイントが追加されます。 この呼び出しに RequireAuthorization をチェーンすると、エンドポイントに承認ポリシーがアタッチされます。

UseAuthenticationUseAuthorization を呼び出すと、認証ミドルウェアと承認ミドルウェアが追加されます。 これらのミドルウェアは、次のことができるように UseRoutingUseEndpoints の間に配置されます。

  • UseRouting によって選択されたエンドポイントを確認します。
  • UseEndpoints によってエンドポイントにディスパッチされる前に、承認ポリシーを適用します。

エンドポイントのメタデータ

前の例には 2 つのエンドポイントがありますが、承認ポリシーがアタッチされているのは正常性チェック エンドポイントだけです。 要求が正常性チェック エンドポイント /healthz と一致した場合、承認チェックが実行されます。 これは、エンドポイントに追加のデータをアタッチできることを示しています。 この追加データは、エンドポイントのメタデータと呼ばれます。

  • メタデータは、ルーティング対応ミドルウェアによって処理できます。
  • メタデータには、任意の .NET 型を使用できます。

ルーティングの概念

ルーティング システムは、ミドルウェア パイプラインを基にして、強力なエンドポイント概念を追加することにより、構築されています。 エンドポイントは、ルーティング、承認、および任意の数の ASP.NET Core システムに関して相互に独立している、アプリの機能の単位を表します。

ASP.NET Core エンドポイントの定義

ASP.NET Core エンドポイントとは次のようなものです。

  • 実行可能: RequestDelegate を持っています。
  • 拡張可能: Metadata コレクションを持っています。
  • Selectable: 必要に応じて、ルーティング情報を持ちます。
  • 列挙可能: エンドポイントのコレクションの一覧は、DI から EndpointDataSource を取得することによって得られます。

次のコードでは、エンドポイントを取得し、現在の要求と一致するものを検査する方法を示します。

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

エンドポイントが選択されている場合は、HttpContext から取得できます。 そのプロパティを検査できます。 エンドポイント オブジェクトは不変であり、作成後に変更することはできません。 最も一般的なエンドポイントの型は RouteEndpoint です。 RouteEndpoint には、ルーティング システムによって選択されるための情報が含まれています。

前述のコードでは、app.Use によってインライン ミドルウェアが構成されます。

次のコードでは、パイプラインで app.Use が呼び出される場所によっては、エンドポイントが存在しない場合があることを示します。

// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

前述のサンプルでは、エンドポイントが選択されているかどうかを表示する Console.WriteLine ステートメントが追加されています。 わかりやすくするため、このサンプルでは、指定された / エンドポイントに表示名が割り当てられています。

前述のサンプルには、これらのミドルウェアがパイプライン内で実行されるタイミングを正確に制御するための UseRouting および UseEndpoints の呼び出しも含まれています。

このコードを / の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

このコード他の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

この出力は次のことを示しています。

  • UseRouting が呼び出される前は、エンドポイントは常に null になっています。
  • 一致が見つかった場合、エンドポイントは UseRoutingUseEndpoints の間で null 以外の値になります。
  • 一致が見つかると、UseEndpoints ミドルウェアはターミナルです。 ターミナル ミドルウェアについては、この記事で後ほど定義します。
  • UseEndpoints の後のミドルウェアは、一致が検出されなかった場合にのみ実行されます。

UseRouting ミドルウェアでは、SetEndpoint メソッドを使用して、エンドポイントが現在のコンテキストにアタッチされます。 UseRouting ミドルウェアをカスタム ロジックに置き換えることができ、その場合でもエンドポイントを使用する利点を得られます。 エンドポイントはミドルウェアのような低レベルのプリミティブであり、ルーティングの実装には結合されません。 ほとんどのアプリでは、UseRouting をカスタム ロジックに置き換える必要はありません。

UseEndpoints ミドルウェアは、UseRouting ミドルウェアと連携して使用するように設計されています。 エンドポイントを実行するためのコア ロジックは複雑ではありません。 GetEndpoint を使用してエンドポイントを取得し、その RequestDelegate プロパティを呼び出します。

次のコードでは、ミドルウェアがルーティングに与える影響またはルーティングに対応する方法を示します。

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

前の例では、2 つの重要な概念が示されています。

  • ミドルウェアは、UseRouting の前に実行して、ルーティングの動作に使用されるデータを変更できます。
  • ミドルウェアは、UseRoutingUseEndpoints の間で実行して、エンドポイントが実行される前にルーティングの結果を処理できます。
    • UseRoutingUseEndpoints の間で実行されるミドルウェア:
      • 通常、メタデータを検査してエンドポイントを認識します。
      • 多くの場合、UseAuthorizationUseCors によって実行されるセキュリティに関する決定を行います。
    • ミドルウェアとメタデータを組み合わせることで、エンドポイントごとにポリシーを構成できます。

上のコードでは、エンドポイントごとのポリシーをサポートするカスタム ミドルウェアの例が示されています。 ミドルウェアによって、機密データへのアクセスの "監査ログ" がコンソールに書き込まれます。 RequiresAuditAttribute メタデータを使用して、エンドポイントを "監査する" ようにミドルウェアを構成できます。 このサンプルでは、機密としてマークされているエンドポイントのみが監査される "オプトイン" パターンを示します。 このロジックの逆を定義して、たとえば安全としてマークされていないすべてのものを監査することができます。 エンドポイント メタデータ システムは柔軟です。 ユース ケースに適したどのような方法でも、このロジックを設計できます。

前のサンプル コードは、エンドポイントの基本的な概念を示すことが意図されています。 サンプルは運用環境での使用は意図されていません。 より完全なバージョンの "監査ログ" ミドルウェアでは、次のことが行われます。

  • ファイルまたはデータベースにログを記録します。
  • ユーザー、IP アドレス、機密性の高いエンドポイントの名前などの詳細情報が追加されます。

コントローラーや SignalR などのクラスベースのフレームワークで簡単に使用できるように、監査ポリシー メタデータ RequiresAuditAttributeAttribute と定義されています。 "コードへのルーティング" を使用すると、次のようになります。

  • メタデータがビルダー API にアタッチされます。
  • エンドポイントを作成するとき、クラス ベースのフレームワークに、対応するメソッドとクラスのすべての属性が組み込まれます。

メタデータの型に対するベスト プラクティスは、インターフェイスまたは属性として定義することです。 インターフェイスと属性では、コードを再利用できます。 メタデータ システムは柔軟であり、どのような制限もありません。

ターミナル ミドルウェアとルーティングを比較する

次の例は、ターミナル ミドルウェアとルーティングの両方を示しています。

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Approach 1: で示されているミドルウェアのスタイルは、ターミナル ミドルウェアです。 ターミナル ミドルウェアと呼ばれるのは、照合操作を実行するためです。

  • 前のサンプルの照合操作は、ミドルウェアの場合は Path == "/"、ルーティングの場合は Path == "/Routing" です。
  • 照合が成功すると、next ミドルウェアを呼び出すのではなく、一部の機能を実行して戻ります。

ターミナル ミドルウェアと呼ばれるのは、検索を終了し、いくつかの機能を実行してから制御を返すためです。

次の一覧では、ターミナル ミドルウェアとルーティングを比較しています。

  • どちらの方法でも、処理パイプラインを終了できます。
    • ミドルウェアでは、next を呼び出すのではなく、戻ることによってパイプラインが終了されます。
    • エンドポイントは常にターミナルです。
  • ターミナル ミドルウェアを使用すると、パイプライン内の任意の場所にミドルウェアを配置できます。
    • エンドポイントは、UseEndpoints の位置で実行されます。
  • ターミナル ミドルウェアでは、任意のコードを使用してミドルウェアが一致するかどうかを判定できます。
    • カスタム ルート一致コードは、冗長で、正しく記述するのが困難な場合があります。
    • ルーティングでは、一般的なアプリに対して簡単なソリューションが提供されます。 ほとんどのアプリでは、カスタム ルート一致コードは必要ありません。
  • UseAuthorizationUseCors などのミドルウェアを使用したエンドポイント インターフェイス。
    • UseAuthorization または UseCors でターミナル ミドルウェアを使用するには、承認システムとの手動インターフェイスが必要です。

エンドポイント では、次の両方が定義されます。

  • 要求を処理するためのデリゲート。
  • 任意のメタデータのコレクション。 メタデータは、各エンドポイントにアタッチされている構成とポリシーに基づいて横断的な関心事を実装するために使用されます。

ターミナル ミドルウェアは効果的なツールになる可能性がありますが、次のものが必要です。

  • 大量のコーディングとテスト。
  • 必要なレベルの柔軟性を実現するための、他のシステムとの手作業による統合。

ターミナル ミドルウェアを作成する前に、ルーティングとの統合を検討してください。

Map または MapWhen と統合されている既存のターミナル ミドルウェアは、通常、ルーティング対応のエンドポイントにすることができます。 MapHealthChecks では、ルーターウェアのパターンが示されています。

  • IEndpointRouteBuilder で拡張メソッドを作成します。
  • CreateApplicationBuilder を使用して、入れ子になったミドルウェア パイプラインを作成します。
  • ミドルウェアを新しいパイプラインにアタッチします。 例では、 UseHealthChecksが使用されます。
  • RequestDelegate にミドルウェア パイプラインを Build します。
  • Map を呼び出し、新しいミドルウェア パイプラインを提供します。
  • 拡張メソッドから Map によって提供されるビルダー オブジェクトを返します。

次のコードでは、MapHealthChecks の使用方法を示します。

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

前のサンプルでは、ビルダー オブジェクトを返すことが重要である理由が示されています。 ビルダー オブジェクトを返すことで、アプリ開発者はエンドポイントの承認などのポリシーを構成できます。 この例では、正常性チェック ミドルウェアと承認システムは直接統合されていません。

そのメタデータ システムは、ターミナル ミドルウェアを使用する機能拡張作成者によって、発生する問題に対応して作成されました。 各ミドルウェアで承認システムとの独自の統合を実装することには問題があります。

URL 一致

  • ルーティングによって受信要求がエンドポイントと照合されるプロセスです。
  • URL パスとヘッダーのデータに基づいています。
  • 要求内の任意のデータを考慮するように拡張できます。

実行されたルーティング ミドルウェアでは、Endpoint が設定され、現在の要求からの HttpContext要求機能に値がルーティングされます。

  • HttpContext.GetEndpoint を呼び出すと、エンドポイントが取得されます。
  • HttpRequest.RouteValues では、ルート値のコレクションが取得されます。

ルーティング ミドルウェアの後で実行されたミドルウェアでは、エンドポイントを調べて、アクションを実行することができます。 たとえば、承認ミドルウェアでは、エンドポイントのメタデータ コレクションに対し、承認ポリシーを問い合わせることができます。 要求処理パイプライン内のすべてのミドルウェアが実行された後、選択したエンドポイントのデリゲートが呼び出されます。

エンドポイント ルーティングのルーティング システムでは、配布に関するすべての決定が行われます。 ミドルウェアでは選択されたエンドポイントに基づいてポリシーが適用されるため、次のことが重要です。

  • ディスパッチまたはセキュリティ ポリシーの適用に影響を与える可能性のある決定は、ルーティング システム内で行われます。

警告

後方互換性のために、コントローラーまたは Razor Pages エンドポイント デリゲートが実行されると、それまでに実行された要求処理に基づいて、RouteContext.RouteData のプロパティが適切な値に設定されます。

RouteContext の種類は、今後のリリースでは古いものとしてマークされます。

  • RouteData.ValuesHttpRequest.RouteValues に移行します。
  • RouteData.DataTokens を移行して、エンドポイント メタデータから IDataTokensMetadata を取得します。

URL の照合は、構成可能な一連のフェーズで動作します。 各フェーズでの出力は一致のセットとなります。 一致のセットは、次のフェーズでさらに絞り込むことができます。 ルーティングの実装では、一致するエンドポイントの処理順序は保証されません。 一致の可能性のあるものは一度にすべて処理されます。 URL 照合フェーズは、次の順序で発生します。 ASP.NET Core:

  1. エンドポイントのセットおよびそれらのルート テンプレートに対して URL パスを処理し、すべての一致を収集します。
  2. 前のリストを取得し、ルート制約が適用されると失敗する一致を削除します。
  3. 前のリストを取得し、MatcherPolicy インスタンスのセットを失敗させる一致を削除します。
  4. EndpointSelector を使用して、前のリストから最終的な決定を行います。

エンドポイントのリストは、次の内容に従って優先度付けが行われます。

EndpointSelector に到達するまで、各フェーズで一致するすべてのエンドポイントが処理されます。 EndpointSelector は最後のフェーズです。 一致の中から最も優先度の高いエンドポイントが最適な一致として選択されます。 最適な一致と優先度が同じである一致が他にもある場合は、あいまい一致の例外がスローされます。

ルートの優先順位はより具体的なルート テンプレートに、より高い優先度が与えられることに基づいて算出されます。 たとえば、テンプレート /hello/{message} を検討してみます。

  • どちらも URL パス /hello と一致します。
  • /hello の方がより具体的であるため、優先度が高くなります。

一般に、ルートの優先順位は、実際に使用される URL スキームの種類として最適なものを選択するのに適しています。 Order は、あいまいさを避けるために必要な場合にのみ使用します。

拡張性の種類がルーティングによって指定されるため、あいまいなルートを事前にルーティング システムによって計算することはできません。 ルート テンプレート /{message:alpha}/{message:int} などの例を考えてみましょう。

  • alpha 制約を使用すると、アルファベット文字のみと一致します。
  • int 制約を使用すると、数値のみと一致します。
  • これらのテンプレートのルート優先順位は同じですが、この両方と一致する単一の URL はありません。
  • 起動時にルーティング システムからあいまいエラーが報告された場合、それによってこの有効なユース ケースはブロックされます。

警告

UseEndpoints 内での処理の順序は、ルーティングの動作には影響しませんが、例外が 1 つあります。 MapControllerRoute および MapAreaRoute では、それぞれが呼び出された順序に基づいて、それぞれのエンドポイントに順序値が自動的に割り当てられます。 これにより、ルーティング システムでより古いルーティング実装と同じ保証を提供しなくても、コントローラーの長時間の動作がシミュレートされます。

ASP.NET Core でのエンドポイントのルーティングは、

  • ルートの概念がありません。
  • 順序付けが保証されません。 すべてのエンドポイントが一度に処理されます。

ルート テンプレートの優先順位とエンドポイントの選択順序

ルート テンプレートの優先順位とは、どれほど具体的であるかに基づいて、各ルート テンプレートに値を割り当てるシステムです。 ルート テンプレートの優先順位:

  • 一般的なケースでは、エンドポイントの順序を調整する必要はなくなります。
  • 一般的に期待されるルーティング動作との一致が試みられます。

たとえば、テンプレート /Products/List/Products/{id} について考えてみます。 URL パス /Products/List に対しては、/Products/List の方が /Products/{id} よりも適していると想定するのが妥当です。 このように言えるのは、リテラル セグメント /List がパラメーター セグメント /{id} よりも優先順位が高いと見なされるためです。

優先順位のしくみの詳細は、ルート テンプレートの定義方法と関連付けられています。

  • より多くのセグメントを持つテンプレートは、より具体的なものと見なされます。
  • リテラル テキストを含むセグメントは、パラメーター セグメントよりも具体的であると見なされます。
  • 制約が含まれるパラメーター セグメントは、それが含まれないものよりも具体的であると見なされます。
  • 複雑なセグメントは、制約を含むパラメーター セグメントと同じくらい具体的であると見なされます。
  • キャッチオール パラメーターは、まったく具体的ではありません。 キャッチオール ルートに関する重要な情報については、「ルート テンプレート」セクションのキャッチオールに関する記述を参照してください。

URL 生成の概念

URL の生成:

  • ルーティングにおいて、一連のルート値に基づいて URL パスを作成するプロセスです。
  • エンドポイントとそれにアクセスする URL を論理的に分離できます。

エンドポイント ルーティングには、LinkGenerator API が含まれます。 LinkGeneratorDI から使用できるシングルトン サービスです。 LinkGenerator API は、実行中の要求のコンテキスト外で使用することができます。 Mvc.IUrlHelper と、タグ ヘルパー、HTML ヘルパー、アクション結果など、IUrlHelper に依存するシナリオでは LinkGenerator API を内部的に使用して、リンク生成機能が提供されます。

リンク ジェネレーターは、アドレスアドレス スキーム の概念に基づいています。 アドレス スキームは、リンク生成で考慮すべきエンドポイントを決定する方法です。 たとえば、コントローラーおよび Razor Pages からの、多くのユーザーに馴染みのあるルート名やルート値シナリオは、アドレス スキームとして実装されます。

リンク ジェネレーターでは、次の拡張メソッドを介して、コントローラーおよび Razor Pages にリンクできます。

これらのメソッドのオーバーロードでは、HttpContext を含む引数が受け入れられます。 これらのメソッドは Url.Action および Url.Page と機能的には同等ですが、柔軟性とオプションがさらに提供されます。

GetPath* メソッドは、絶対パスを含む URI を生成するという点で Url.Action および Url.Page に最も似ています。 GetUri* メソッドでは常に、スキームとホストを含む絶対 URI が生成されます。 HttpContext を受け入れるメソッドでは、実行中の要求のコンテキストで URI が生成されます。 実行中の要求からのアンビエント ルート値、URL ベース パス、スキーム、およびホストは、オーバーライドされない限り使用されます。

LinkGenerator はアドレスと共に呼び出されます。 URI の生成は、次の 2 つの手順で行われます。

  1. アドレスは、そのアドレスと一致するエンドポイントのリストにバインドされます。
  2. 各エンドポイントの RoutePattern は、指定された値と一致するルート パターンが見つかるまで評価されます。 結果の出力は、リンク ジェネレーターに指定された他の URI 部分と結合され、返されます。

LinkGenerator によって提供されるメソッドでは、すべての種類のアドレスの標準的なリンク生成機能がサポートされます。 リンク ジェネレーターを使用する最も便利な方法は、特定のアドレスの種類の操作を実行する拡張メソッドを使用することです。

拡張メソッド 説明
GetPathByAddress 指定された値に基づき、絶対パスを含む URI を生成します。
GetUriByAddress 指定された値に基づき、絶対 URI を生成します。

警告

LinkGenerator メソッド呼び出しによる次の影響に注意してください。

  • 受信要求の Host ヘッダーが確認されないアプリ構成では、GetUri* 拡張メソッドは注意して使用してください。 受信要求の Host ヘッダーが確認されていない場合、信頼されていない要求入力を、ビューまたはページの URI でクライアントに送り返すことができます。 すべての運用アプリで、Host ヘッダーを既知の有効な値と照らし合わせて確認するようにサーバーを構成することをお勧めします。

  • ミドルウェアで Map または MapWhen と組み合わせて、LinkGenerator を使用する場合は注意してください。 Map* では、実行中の要求の基本パスが変更され、リンク生成の出力に影響します。 すべての LinkGenerator API で基本パスを指定することができます。 リンク生成への Map* の影響を元に戻すための空の基本パスを指定してください。

ミドルウェアの例

次の例では、ミドルウェアで LinkGenerator API を使用して、商品をリストするアクション メソッドへのリンクを作成します。 リンク ジェネレーターは、クラスに挿入し、GenerateLink を呼び出すことで、アプリのどのクラスでも使用できます。

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

ルート テンプレート

{} 内のトークンでは、ルートが一致した場合にバインドされるルート パラメーターが定義されます。 1 つのルート セグメントに複数のルート パラメーターを定義できますが、各ルート パラメーターをリテラル値で区切る必要があります。 次に例を示します。

{controller=Home}{action=Index}

これは、{controller}{action} の間にリテラル値がないため、有効なルートではありません。 ルート パラメーターには名前を付ける必要があります。付加的な属性を指定することもあります。

ルート パラメーター以外のリテラル テキスト ({id} など) とパス区切り / は URL のテキストに一致する必要があります。 テキスト照合は復号された URL のパスを基盤とし、大文字と小文字が区別されます。 リテラル ルート パラメーターの区切り記号 ({ または }) を照合するには、文字を繰り返して区切り記号をエスケープします。 たとえば、{{ または }} です。

アスタリスク * または二重アスタリスク**:

  • ルート パラメーターのプレフィックスとして使用して、URI の残りの部分にバインドすることができます。
  • キャッチオール パラメーターと呼ばれています。 blog/{**slug} の例を次に示します。
    • blog/ で始まり、その後に任意の値が続く URI と一致します。
    • blog/ に続く値は、slug ルート値に割り当てられます。

警告

ルーティングでバグが原因で、キャッチオール パラメーターがルートと正しく一致しない可能性があります。 このバグの影響を受けるアプリには、次の特性があります。

  • キャッチオール ルート (たとえば、{**slug}")
  • キャッチオール ルートが、一致すべき要求と一致しません。
  • 他のルートを削除すると、キャッチオール ルートが機能し始めます。

このバグが発生するケースの例については、GitHub のバグ 18677 および 16579 を参照してください。

このバグのオプトイン修正は .NET Core 3.1.301 SDK 以降に含まれています。 次のコードにより、このバグを修正する内部スイッチが設定されます。

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

キャッチオール パラメーターは空の文字列に一致することもあります。

キャッチオール パラメーターでは、パス区切り / 文字を含め、URL の生成にルートが使用されるときに適切な文字がエスケープされます。 たとえば、ルート値が { path = "my/path" } のルート foo/{*path} では、foo/my%2Fpath が生成されます。 エスケープされたスラッシュに注意してください。 パス区切り文字をラウンドトリップさせるには、** ルート パラメーター プレフィックスを使用します。 { path = "my/path" } のルート foo/{**path} では、foo/my/path が生成されます。

任意のファイル拡張子が付いたファイル名のキャプチャを試行する URL パターンには、追加の考慮事項があります。 たとえば、テンプレート files/{filename}.{ext?} について考えてみます。 filenameext の両方の値が存在するときに、両方の値が入力されます。 URL に filename の値だけが存在する場合、末尾の . は任意であるため、このルートは一致となります。 次の URL はこのルートに一致します。

  • /files/myFile.txt
  • /files/myFile

ルート パラメーターには、既定値 が含まれることがあります。パラメーター名の後に既定値を指定し、等号 (=) で区切ることで指定されます。 たとえば、{controller=Home} では、controller の既定値として Home が定義されます。 パラメーターの URL に値がない場合、既定値が使用されます。 ルート パラメーターは、パラメーター名の終わりに疑問符 (?) を追加することでオプションとして扱われます。 たとえば、id? のようにします。 省略可能な値と既定のルート パラメーターの違いは次のとおりです。

  • 既定値を持つルート パラメーターでは常に値が生成されます。
  • 省略可能なパラメーターには、要求 URL によって値が指定された場合にのみ値が含められます。

ルート パラメーターには、URL からバインドされるルート値に一致しなければならないという制約が含まれることがあります。 : と制約名をルート パラメーター名の後に追加すると、ルート パラメーターのインライン制約が指定されます。 その制約で引数が要求される場合、制約名の後にかっこ (...) で囲まれます。 複数の "インライン制約" を指定するには、別の : と制約名を追加します。

制約名と引数が IInlineConstraintResolver サービスに渡され、URL 処理で使用する IRouteConstraint のインスタンスが作成されます。 たとえば、ルート テンプレート blog/{article:minlength(10)} によって、制約 minlength と引数 10 が指定されます。 ルート制約の詳細とこのフレームワークによって指定される制約のリストについては、「ルート制約」セクションを参照してください。

ルート パラメーターには、パラメーター トランスフォーマーを指定することもできます。 パラメーター トランスフォーマーを指定すると、リンクを生成し、アクションおよびページを URL と一致させるときにパラメーターの値が変換されます。 制約と同様に、パラメーター トランスフォーマーをルート パラメーターにインラインで追加することができます。その場合、ルート パラメーター名の後に : とトランスフォーマー名を追加します。 たとえば、ルート テンプレート blog/{article:slugify} では、slugify トランスフォーマーが指定されます。 パラメーター トランスフォーマーの詳細については、「パラメーター トランスフォーマー」セクションを参照してください。

次の表に、ルート テンプレートの例とその動作を示します。

ルート テンプレート 一致する URI の例 要求 URI
hello /hello 単一パス /hello にのみ一致します。
{Page=Home} / 一致し、PageHome に設定されます。
{Page=Home} /Contact 一致し、PageContact に設定されます。
{controller}/{action}/{id?} /Products/List Products コントローラーと List アクションにマッピングされます。
{controller}/{action}/{id?} /Products/Details/123 Products コントローラーと Details アクションにマッピングされ、id は 123 に設定されます。
{controller=Home}/{action=Index}/{id?} / Home コントローラーと Index メソッドにマッピングされます。 id は無視されます。
{controller=Home}/{action=Index}/{id?} /Products Products コントローラーと Index メソッドにマッピングされます。 id は無視されます。

一般的に、テンプレートの利用が最も簡単なルーティングの手法となります。 ルート テンプレート以外では、制約と既定値も指定できます。

複雑なセグメント

複雑なセグメントは、リテラル区切り文字を右から左に最短一致の方法で照合することによって処理されます。 たとえば、[Route("/a{b}c{d}")] は複雑なセグメントです。 複雑なセグメントは、それらを適切に使用する上で理解する必要がある特定の方法で機能します。 このセクションの例では、パラメーター値の中に区切り文字が含まれていない場合にのみ、複雑なセグメントが本当にうまく機能する理由を示します。 より複雑なケースでは、regex を使用し、値を手動で抽出する必要があります。

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

これは、ルーティングがテンプレート /a{b}c{d} と URL パス /abcd を使用して実行するステップの概要です。 | は、アルゴリズムの動作を視覚化するために使用されます。

  • 最初のリテラル (右から左へ) は c です。 そこで、/abcd は右から検索され、/ab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /ab|c|d は中断したところから検索されて、a が見つかり、/|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • 残りのテキストも残りのルート テンプレートも存在しないため、これは一致となります。

同じテンプレート /a{b}c{d} と、URL パス /aabcd を使用した場合の否定の例を次に示します。 | は、アルゴリズムの動作を視覚化するために使用されます。 このケースは一致ではありませんが、同じアルゴリズムで説明します。

  • 最初のリテラル (右から左へ) は c です。 そこで、/aabcd は右から検索され、/aab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /aab|c|d は中断したところから検索されて、a が見つかり、/a|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • この時点で、テキスト a が残っていますが、アルゴリズムは解析するためのルート テンプレートを使い果たしたので、これは一致とはなりません。

照合アルゴリズムは最短一致のため、次のようになります。

  • 各ステップで可能な限りの最短のテキストに一致します。
  • パラメーター値の内部に区切り記号の値が表示されている場合は一致しません。

正規表現を使用すると、一致の動作をより細かく制御できます。

貪欲マッチは、最長マッチとも呼ばれ、正規表現パターンを満たす入力テキスト内で可能な限り長い一致を見つけようとします。 非貪欲マッチは、最短マッチとも呼ばれ、正規表現パターンを満たす入力テキスト内で可能な限り短い一致を探します。

特殊文字を含むルーティング

特殊文字を含むルーティングは、予期しない結果になる可能性があります。 たとえば、次のアクション メソッドを含むコントローラーについて考えます。

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

string id に次のエンコードされた値が含まれる場合、予期しない結果が発生する可能性があります。

ASCII Encoded
/ %2F
+

ルート パラメーターは常に URL でデコードされるとは限りません。 この問題は今後対処される可能性があります。 詳しくは、こちらの GitHub イシューに関するページをご覧ください。

ルート制約

ルート制約は、受信 URL と一致し、URL パスがルート値にトークン化されたときに実行されます。 ルート制約では、通常、ルート テンプレート経由で関連付けられるルート値を調べ、値が許容できるかどうかを true または false で決定します。 一部のルート制約では、ルート値以外のデータを使用し、要求をルーティングできるかどうかが考慮されます。 たとえば、HttpMethodRouteConstraint はその HTTP Verb に基づいて要求を承認または却下します。 制約は、要求のルーティングとリンクの生成で使用されます。

警告

入力の検証には制約を使用しないでください。 入力の検証に制約を使用した場合、入力が無効だと "404 見つかりません" が返されます。 無効な入力の場合は、"400 要求が無効です" と適切なエラー メッセージが生成されます。 ルート制約は、特定のルートに対する入力の妥当性を検証するためではなく、似たようなルートの違いを明らかにするために使用されます。

次の表では、ルート制約の例とそれに求められる動作をまとめています。

制約 一致の例 メモ
int {id:int} 123456789-123456789 あらゆる整数に一致する
bool {active:bool} trueFALSE true または false に一致する。 大文字と小文字は区別されない
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm インバリアント カルチャの有効な DateTime 値に一致します。 前の警告を参照してください。
decimal {price:decimal} 49.99-1,000.01 インバリアント カルチャの有効な decimal 値に一致します。 前の警告を参照してください。
double {weight:double} 1.234-1,001.01e8 インバリアント カルチャの有効な double 値に一致します。 前の警告を参照してください。
float {weight:float} 1.234-1,001.01e8 インバリアント カルチャの有効な float 値に一致します。 前の警告を参照してください。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 有効な Guid 値に一致する
long {ticks:long} 123456789-123456789 有効な long 値に一致する
minlength(value) {username:minlength(4)} Rick 4 文字以上の文字列であることが必要
maxlength(value) {filename:maxlength(8)} MyFile 8 文字以内の文字列であることが必要
length(length) {filename:length(12)} somefile.txt 厳密に 12 文字の文字列であることが必要
length(min,max) {filename:length(8,16)} somefile.txt 8 文字以上、16 文字以内の文字列であることが必要
min(value) {age:min(18)} 19 18 以上の整数値であることが必要
max(value) {age:max(120)} 91 120 以下の整数値であることが必要
range(min,max) {age:range(18,120)} 91 18 以上、120 以下の整数値であることが必要
alpha {name:alpha} Rick 文字列は 1 つまたは複数のアルファベット文字で構成されることが必要 (a-z、大文字と小文字は区別されません)。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 文字列は正規表現と一致する必要があります。 正規表現の定義に関するヒントを参照してください。
required {name:required} Rick URL 生成中、非パラメーターが提示されるように強制する

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

1 のパラメーターには、複数の制約をコロンで区切って適用できます。 たとえば、次の制約では、パラメーターが 1 以上の整数値に制限されます。

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

警告

URL の妥当性を検証し、CLR 型に変換するルート制約では、常にインバリアント カルチャが使用されます。 たとえば、int または DateTime の CLR 型に変換される場合などです。 これらの制約では、URL のローカライズが不可であることが前提です。 フレームワークから提供されるルート制約がルート値に格納されている値を変更することはありません。 URL から解析されたルート値はすべて文字列として格納されます。 たとえば、float 制約はルート値を浮動小数に変換しますが、変換された値は、浮動小数に変換できることを検証するためにだけ利用されます。

正規表現の制約

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

正規表現は、regex(...) ルート制約を使用して、インライン制約として指定できます。 MapControllerRoute ファミリのメソッドでも、制約のオブジェクト リテラルを取ります。 この形式が使用されている場合、文字列値は正規表現として解釈されます。

次のコードでは、インラインで regex 制約が使用されています。

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

次のコードでは、regex 制約の指定にオブジェクト リテラルが使用されています。

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

ASP.NET Core フレームワークでは、正規表現コンストラクターに RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant が追加されます。 これらのメンバーの詳細については、「RegexOptions」を参照してください。

正規表現では、ルーティングや C# 言語で使用されるものに似た区切り記号とトークンが使用されます。 正規表現トークンはエスケープする必要があります。 インライン制約で正規表現 ^\d{3}-\d{2}-\d{4}$ を使用するには、次のいずれかを使用します。

  • \ 文字列エスケープ文字をエスケープするには、文字列で指定した \ 文字を、C# ソース ファイル内の \\ 文字に置き換えます。
  • 逐語的文字列リテラル

ルーティング パラメーター区切り記号文字 ({}[]) をエスケープするには、表現の文字を二重にします (例: {{}}[[]])。 次の表に、正規表現とそれにエスケープを適用した後のものを示します。

正規表現 エスケープ適用後の正規表現
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

ルーティングで使用される正規表現は、多くの場合、^ 文字で始まり、これは文字列の開始位置と一致します。 この式は、多くの場合、$ 文字で終わり、文字列の末尾と一致します。 ^ 文字と $ 文字により、正規表現がルート パラメーター値全体に一致することが保証されます。 ^ 文字と $ 文字がなければ、意図に反し、正規表現は文字列内のあらゆる部分文字列に一致してしまいます。 下の表では、一致または不一致の理由を例を示し説明します。

正規表現 String 一致したもの コメント
[a-z]{2} hello はい サブ文字列の一致
[a-z]{2} 123abc456 はい サブ文字列の一致
[a-z]{2} mz はい 一致する表現
[a-z]{2} MZ はい 大文字と小文字の使い方が違う
^[a-z]{2}$ hello いいえ 上の ^$ を参照
^[a-z]{2}$ 123abc456 いいえ 上の ^$ を参照

正規表現構文の詳細については、.NET Framework 正規表現に関するページを参照してください。

既知の入力可能値の集まりにパラメーターを制限するには、正規表現を使用します。 たとえば、{action:regex(^(list|get|create)$)} の場合、action ルート値は listgetcreate とのみ照合されます。 制約ディクショナリに渡された場合、文字列 ^(list|get|create)$ で同じものになります。 既知の制約に一致しない、制約ディクショナリに渡された制約も、正規表現として扱われます。 既知の制約に一致しない、テンプレート内で渡される制約は、正規表現としては扱われません。

カスタム ルート制約

カスタム ルート制約は、IRouteConstraint インターフェイスを実装して作成できます。 IRouteConstraint インターフェイスには、Match が含まれています。これでは、制約が満たされている場合は true を返し、それ以外の場合は false を返します。

カスタム ルート制約は通常必要ありません。 カスタム ルート制約を実装する前に、モデル バインドなどの代替手段を検討してください。

ASP.NET Core の Constraints フォルダーには、制約を作成するための適切な例が用意されています。 たとえば、GuidRouteConstraint です。

カスタムの IRouteConstraint を使うには、サービス コンテナー内の ConstraintMap に、ルート制約の種類が登録されている必要があります。 ConstraintMap は、ルート制約キーを、その制約を検証する IRouteConstraint の実装にマッピングするディクショナリです。 アプリの ConstraintMap は、Program.cs で、AddRouting 呼び出しの一部として、または builder.Services.Configure<RouteOptions> を使って RouteOptions を直接構成することで、更新できます。 次に例を示します。

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

上記の制約は、次のコードに適用されます。

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

NoZeroesRouteConstraint を実装することにより、ルート パラメーターで 0 が使用されるのを回避できます。

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

上記のコードでは次の操作が行われます。

  • ルートの {id} セグメントの 0 を禁止します。
  • カスタム制約を実装する基本的な例を示しています。 実稼働しているアプリでは使用しないでください。

次のコードは、0 を含む id が処理されないようにする優れた方法です。

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

上記のコードには NoZeroesRouteConstraint アプローチに対し、次の利点があります。

  • カスタム制約が必要ありません。
  • ルート パラメーターに 0 が含まれている場合は、よりわかりやすいエラーが返されます。

パラメーター トランスフォーマー

パラメーター トランスフォーマー:

たとえば、Url.Action(new { article = "MyTestArticle" }) のルート パターン blog\{article:slugify} のカスタム slugify パラメーター トランスフォーマーでは、blog\my-test-article が生成されます。

IOutboundParameterTransformer の次の実装を見てみましょう。

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

ルート パターンでパラメーター トランスフォーマーを使用するには、これを Program.csConstraintMap を使用して構成します。

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

ASP.NET Core フレームワークでは、エンドポイントを解決する URI の変換にパラメーター トランスフォーマーを使用します。 たとえば、パラメーター トランスフォーマーでは、areacontrolleractionpage を照合するために使用されるルート値が変換されます。

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

上記のルート テンプレートでは、アクション SubscriptionManagementController.GetAll は URI /subscription-management/get-all と照合されます。 パラメーター トランスフォーマーでは、リンクを生成するために使用されるルート値は変更されません。 たとえば、Url.Action("GetAll", "SubscriptionManagement") では /subscription-management/get-all が出力されます。

ASP.NET Core には、生成されたルートでパラメーター トランスフォーマーを使用する API 規則があります。

URL 生成参照

このセクションには、URL の生成で実装するアルゴリズムの参照情報が含まれています。 実際には、URL 生成の最も複雑な例で、コントローラーまたは Razor Pages が使用されます。 詳細については、コントローラーでのルーティングに関するページを参照してください。

URL の生成プロセスは、LinkGenerator.GetPathByAddress、または類似のメソッドへの呼び出しで開始されます。 このメソッドは、アドレス、一連のルート値、およびオプションで HttpContext からの現在の要求に関する情報と共に渡されます。

まずは、アドレスを使用して、アドレスの型に一致する IEndpointAddressScheme<TAddress> を使用して、一連の候補のエンドポイントが解決されます。

アドレス スキームによって一連の候補が検出されると、URL の生成操作が成功するまで、エンドポイントは反復的に並べ替えられ、処理されます。 URL が生成される際には、あいまいさの確認は行われず、最初に返される結果が最終的な結果になります。

ログを使用した URL 生成のトラブルシューティング

URL の生成のトラブルシューティングを行う場合、まずは Microsoft.AspNetCore.Routing のログ記録レベルを TRACE に設定します。 LinkGenerator では、問題のトラブルシューティングに役立つ、処理に関する多くの詳細がログに記録されます。

URL 生成の詳細については、「URL 生成参照」を参照してください。

アドレス

アドレスとは、リンク ジェネレーターへの呼び出しを一連の候補エンドポイントにバインドするために使用する、URL 生成の概念です。

アドレスとは、次の 2 つの実装を既定で備えた拡張可能な概念です。

  • アドレスとして "エンドポイント名" (string) を使用します。
    • MVC のルート名と同様の機能があります。
    • IEndpointNameMetadata メタデータ型を使用します。
    • 指定された文字列を、登録されているすべてのエンドポイントのメタデータに対して解決します。
    • 複数のエンドポイントが同じ名前を使用している場合は、起動時に例外をスローします。
    • コントローラーと Razor Pages 以外で汎用的に使用する場合にお勧めします。
  • ルート値 (RouteValuesAddress) をアドレスとして使用すると、次のようになります。
    • コントローラーおよび Razor Pages での従来の URL 生成と同様の機能があります。
    • 拡張およびデバッグする場合に非常に複雑です。
    • IUrlHelper、タグ ヘルパー、HTML ヘルパー、アクションの結果などで使用される実装を提供します。

アドレス スキームの役割は、任意の条件によって、アドレスと一致するエンドポイント間の関連付けを作成することです。

  • エンドポイント名スキームでは、基本的な辞書検索が実行されます。
  • ルート値のスキームには、セット アルゴリズムの複雑な最良のサブセットがあります。

アンビエント値と明示的な値

ルーティングは、現在の要求から現在の要求 HttpContext.Request.RouteValues のルート値にアクセスします。 現在の要求に関連付けられている値は、アンビエント値と呼ばれます。 このドキュメントでは、わかりやすくするために、メソッドに渡されるルート値を明示的な値と呼びます。

次の例では、アンビエント値と明示的な値を示しています。 ここでは、現在の要求のアンビエント値と明示的な値を提供します。

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

上記のコードでは次の操作が行われます。

  • /Widget/Index/17 を返します。
  • DI を介して LinkGenerator を取得します。

次のコードでは、明示的な値のみ提供されており、アンビエント値は提供されていません。

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

前述のメソッドでは /Home/Subscribe/17 が返されます。

WidgetController の次のコードでは、/Widget/Subscribe/17 が返されます。

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

次のコードは、現在の要求のアンビエント値と明示的な値からコントローラーを提供します。

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

上のコードでは以下の操作が行われます。

  • /Gadget/Edit/17 が返されます。
  • UrlIUrlHelper を取得します。
  • Action が、アクション メソッドの絶対パスを使用して URL を生成します。 URL には、指定した action 名と route 値が含まれます。

次のコードでは、現在の要求のアンビエント値と明示的な値を提供します。

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

前述のコードでは、Edit Razor ページに次のページ ディレクティブが含まれている場合に、url/Edit/17 に設定します。

@page "{id:int}"

[編集] ページに "{id:int}" ルート テンプレートが含まれない場合は、url/Edit?id=17 になります。

MVC の IUrlHelper の動作により、ここで説明するルールに加えて、複雑なレイヤーが追加されます。

  • IUrlHelper は、常に現在の要求からルート値をアンビエント値として提供します。
  • IUrlHelper.Action は、開発者がオーバーライドしない場合を除き、常に現在の actioncontroller ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の page ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の handler ルート値を明示的な値として null にオーバーライドします。

ユーザーは、MVC が独自のルールに従っていないように見えるため、アンビエント値の動作の詳細にしばしば驚きます。 これまでの経緯および互換性の理由により、actioncontrollerpagehandler などの特定のルート値には、独自の特殊な動作があります。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage の同等の機能では、互換性のために IUrlHelper のこの異常と同じ動作が行われます。

URL の生成処理

一連の候補エンドポイントが見つかると、URL の生成アルゴリズムでは次が実行されます。

  • エンドポイントが反復処理されます。
  • 最初に成功した結果が返されます。

このプロセスの最初の手順はルート値の無効化と呼ばれます。 ルート値の無効化は、アンビエント値からどのルート値を使用する必要があり、無視する必要があるかをルーティングが決定するプロセスです。 各アンビエント値が検討され、明示的な値と組み合わされるか、または無視されます。

アンビエント値の役割について一番わかりやすい考え方は、一部の一般的なケースでアプリケーション開発者の入力作業が省かれるということです。 従来、アンビエント値が役に立つシナリオは MVC に関連しています。

  • 同じコントローラー内の別のアクションにリンクする場合、コントローラー名を指定する必要はありません。
  • 同じ領域内の別のコントローラーにリンクする場合、領域名を指定する必要はありません。
  • 同じアクション メソッドにリンクする場合は、ルート値を指定する必要はありません。
  • アプリの別の部分にリンクする場合は、アプリのその部分には意味のないルート値は引き継ぎません。

null を返す LinkGenerator または IUrlHelper の呼び出しは、通常、ルート値の無効化について理解していないことが原因で発生します。 ルート値の無効化のトラブルシューティングを行うには、さらにルート値を明示的に指定して、これにより問題が解決されるかどうかを確認します。

ルート値の無効化は、アプリの URL スキームが階層的であり、階層が左から右に形成されていることを前提として機能します。 基本的なコントローラー ルート テンプレート {controller}/{action}/{id?} について考えてみましょう。これが実際にどのように動作するかを直感的に理解できます。 値に対する変更により、右側に表示されるすべてのルート値が無効化されます。 これには、階層に関する前提が反映されています。 アプリに id のアンビエント値があり、操作によって controller に対して異なる値が指定された場合、

  • {controller}{id?} の左側にあるため、id は再利用されません。

この原則を示すいくつかの例を次に示します。

  • 明示的な値に id の値が含まれている場合、id のアンビエント値は無視されます。 controlleraction のアンビエント値を使用できます。
  • 明示的な値に action の値が含まれている場合、action のアンビエント値はすべて無視されます。 controller のアンビエント値を使用できます。 action の明示的な値が action のアンビエント値と異なる場合、id 値は使用されません。 action の明示的な値が action のアンビエント値と同じ場合、id 値を使用できます。
  • 明示的な値に controller の値が含まれている場合、controller のアンビエント値はすべて無視されます。 controller の明示的な値が controller のアンビエント値と異なる場合、actionid の値は使用されません。 controller の明示的な値が controller のアンビエント値と同じ場合、actionid の値を使用できます。

このプロセスは、属性ルートと専用規則ルートが存在することでさらに複雑になります。 {controller}/{action}/{id?} などのコントローラーの規則ルートでは、ルート パラメーターを使用して階層が指定されます。 コントローラーと Razor Pages に対する専用規則ルート属性ルートの場合、

  • ルート値の階層があります。
  • テンプレートには表示されません。

このような場合は、URL の生成によって必要な値の概念が定義されます。 コントローラーおよび Razor Pages によって作成されたエンドポイントには、ルート値の無効化を機能させるために必要な値が指定されています。

ルート値の無効化アルゴリズムの詳細は次のとおりです。

  • 必要な値の名前がルート パラメーターと組み合わされ、左から右に処理されます。
  • 各パラメーターについて、アンビエント値と明示的な値が比較されます。
    • アンビエント値と明示的な値が同じ場合、プロセスは続行されます。
    • アンビエント値が存在し、明示的な値が存在しない場合は、URL を生成するときにアンビエント値が使用されます。
    • アンビエント値が存在せず、明示的な値が存在する場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。
    • アンビエント値と明示的な値が存在し、2 つの値が異なる場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。

この時点で、URL の生成操作はルート制約を評価する準備ができています。 許容可能な値のセットがパラメーターの既定値と組み合わされ、制約に提供されます。 すべての制約について合格した場合、操作が続行されます。

次に、許容可能な値を使用してルート テンプレートを展開できます。 ルート テンプレートは次のように処理されます。

  • 左から右。
  • 各パラメーターに、許容可能な値が代入されます。
  • 次の特殊なケースがあります。
    • 許容可能な値がなく、パラメーターに既定値がある場合は、既定値が使用されます。
    • 許容可能な値がなく、パラメーターが省略可能な場合は、処理が続行されます。
    • 存在しない省略可能なパラメーターの右側にあるルート パラメーターのいずれかに値がある場合、操作は失敗します。
    • 連続する既定値パラメーターと省略可能なパラメーターは、可能な場合、折りたたまれています。

ルートのセグメントと一致しない明示的に指定された値は、クエリ文字列に追加されます。 次の表は、ルート テンプレート {controller}/{action}/{id?} の使用時の結果をまとめたものです。

アンビエント値 明示的な値 結果
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

省略可能なルート パラメーターの順序

省略可能なルート パラメーターは、すべての必須のルート パラメーターとリテラルの後に指定する必要があります。 次のコードで、idname のパラメーターは color パラメーターの後に指定する必要があります。

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}

ルート値の無効化に関する問題

次のコードは、ルーティングでサポートされていない URL 生成スキームの例を示しています。

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

上記のコードでは、culture ルート パラメーターがローカライズに使用されています。 culture パラメーターを常にアンビエント値として許容されるようにすることが望まれます。 しかし、必要な値の動作方法が理由で、culture パラメーターはアンビエント値として許容されません。

  • "default" ルート テンプレートでは、culture ルート パラメーターは controller の左側にあるため、controller を変更しても culture は無効になりません。
  • "blog" ルート テンプレートでは、culture ルート パラメーターは controller の右側にあると見なされ、必要な値に表示されます。

LinkParser で URL パスを解析する

LinkParser クラスは、URL パスをルート値のセットに解析するためのサポートを追加します。 ParsePathByEndpointName メソッドは、エンドポイント名と URL パスを受け取り、URL パスから抽出された一連のルート値を返します。

次のコントローラーの例では、GetProduct アクションは api/Products/{id} のルート テンプレートを使用し、GetProduct という Name を持ちます。

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

同じコントローラー クラス内で、AddRelatedProduct アクションには URL パス pathToRelatedProduct が必要です。これは、クエリ文字列パラメーターとして指定できます。

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

前の例では、AddRelatedProduct アクションによって URL パスから id ルート値が抽出されます。 たとえば、/api/Products/1 の URL パスが指定されている場合、relatedProductId 値は 1 に設定されます。 この方法により、API のクライアントは、リソースを参照するときに URL パスを使用できます。そのような URL の構造に関する知識は必要ありません。

エンドポイント メタデータを構成する

次のリンクでは、エンドポイント メタデータを構成する方法に関する情報を提供しています。

RequireHost とルートが一致するホスト

RequireHost では、指定したホストが必要であるという制約がルートに適用されます。 RequireHost または [Host] パラメーターには、以下を指定できます。

  • ホスト: www.domain.com。任意のポートの www.domain.com と一致します。
  • ホストとワイルドカード: *.domain.com。任意のポートの www.domain.comsubdomain.domain.com、または www.subdomain.domain.com と一致します。
  • ポート: *:5000。任意のホストのポート 5000 と一致します。
  • ホストとポート: www.domain.com:5000 または *.domain.com:5000。ホストとポートと一致します。

RequireHost または [Host] を使用して、複数のパラメーターを指定できます。 制約は、いずれかのパラメーターに対して有効なホストと一致します。 たとえば、[Host("domain.com", "*.domain.com")]domain.comwww.domain.com、および subdomain.domain.com と一致します。

次のコードでは、RequireHost を使用して、指定したホストをルートに対して要求します。

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

次のコードでは、コントローラー上の [Host] 属性を使用して、指定したホストのいずれかを要求します。

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

[Host] 属性がコントローラーとアクション メソッドの両方に適用される場合は、次のようになります。

  • アクションの属性が使用されます。
  • コントローラーの属性は無視されます。

警告

HttpRequest.HostRequireHost などのホスト ヘッダーに依存する API は、クライアントによるスプーフィングの対象になる可能性があります。

ホストとポートのスプーフィングを防ぐには、次のいずれかの方法を使用します。

ルート グループ

MapGroup 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、エンドポイント メタデータを追加する RequireAuthorizationWithMetadata のようなメソッドを 1 回呼び出すだけで、エンドポイントのグループ全体をカスタマイズできます。

たとえば、次のコードにより、2 つの似たエンドポイント グループが作成されます。

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

このシナリオでは、201 Created 結果の Location ヘッダーに相対アドレスを使用できます。

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

エンドポイントの最初のグループは、/public/todos のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 2 番目のグループは、/private/todos のプレフィックスが付いた要求にのみ一致し、認証が必要です。

QueryPrivateTodosエンドポイント フィルター ファクトリは、プライベート todo データにアクセスして格納できるようにルート ハンドラーの TodoDb パラメーターを変更するローカル関数です。

ルート グループでは、ルート パラメーターと制約を含む入れ子になったグループと複雑なプレフィックス パターンもサポートされます。 次の例で、user グループにマップされたルート ハンドラーは、外部グループ プレフィックスで定義されている {org} および {group} ルート パラメーターをキャプチャできます。

プレフィックスは空にすることもできます。 これは、ルート パターンを変更せずにエンドポイントのグループにエンドポイント メタデータまたはフィルターを追加する場合に役立ちます。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

フィルターまたはメタデータをグループに追加すると、内部グループまたは特定のエンドポイントに追加された可能性のある追加のフィルターまたはメタデータを追加する前に各エンドポイントに個別に追加する場合と同じように動作します。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

上記の例では、外部フィルターは、2 番目に追加された場合でも、内部フィルターの前に受信要求をログに記録します。 フィルターは異なるグループに適用されているため、互いが相対的に追加された順序は関係ありません。 同じグループまたは特定のエンドポイントに適用されている場合、追加される順序フィルターは重要です。

/outer/inner/ に対する要求によって、次がログに記録されます。

/outer group filter
/inner group filter
MapGet filter

ルーティングに関するパフォーマンス ガイダンス

アプリにパフォーマンス上の問題がある場合、多くの場合ルーティングが問題として疑われます。 ルーティングが疑われる理由は、コントローラーや Razor Pages などのフレームワークにより、フレームワーク内で費やされた時間がログ メッセージで報告されるためです。 コントローラーによって報告された時間と要求の合計時間の間に大きな違いがある場合、次のようになります。

  • 開発者は、問題の発生源としてアプリ コードを排除します。
  • ルーティングが原因であると考えるのが一般的です。

ルーティングは、数千のエンドポイントを使用してパフォーマンス テストされています。 一般的なアプリでは、大きすぎるだけでパフォーマンスの問題が発生する可能性はほとんどありません。 ルーティングのパフォーマンス低下の最も一般的な根本原因は、通常、正しく動作していないカスタム ミドルウェアです。

次のコード サンプルは、遅延の原因を絞り込むための基本的な手法を示したものです。

var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

ルーティングの時間は次のように計ります。

  • 各ミドルウェアを、上記のコードに示されている時間を計るミドルウェアのコピーでインターリーブします。
  • 計られた時間データをコードと関連付けるための一意の識別子を追加します。

これは、遅延が 10ms を超えるなど顕著な場合に絞り込むための基本的な方法です。 Time 1 から Time 2 を引くことで、UseRouting ミドルウェア内で費やされた時間を報告します。

次のコードでは、前の時間を計るコードに対して、よりコンパクトなアプローチを使用します。

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");

潜在的にコストが高いルーティング機能

次の一覧に、基本的なルート テンプレートと比べて比較的コストが高いルーティング機能についての洞察を示します。

  • 正規表現: 複雑な正規表現を作成すること、つまり少量の入力で長い実行時間を実現することができます。
  • 複雑なセグメント ({x}-{y}-{z}):
    • 通常の URL パス セグメントを解析するよりもかなりコストがかかります。
    • より多くの部分文字列が割り当てられることになります。
  • 同期データ アクセス: 多くの複雑なアプリでは、ルーティングの一部としてデータベースにアクセスします。 非同期的な MatcherPolicyEndpointSelectorContext などの拡張ポイントを使用します。

大規模なルート テーブルに関するガイダンス

既定では ASP.NET Core によって、メモリを CPU 時間とトレードするルーティング アルゴリズムが使用されます。 その効果として、ルート照合の時間が、照合するパスの長さにのみ左右され、ルートの数には左右されなくなります。 ただし、アプリに多数 (何千個) のルートがあり、大量の変数のプレフィックスがルートにあるようなケースでは、この方式が問題となる可能性があります。 たとえば、{parameter}/some/literal など、ルートの前方のセグメントにパラメーターが存在する場合があります。

以下のケースを除き、この方式が問題となる状況にアプリが陥る可能性は極めて低いです。

  • このパターンを使用するアプリに多数のルートがある。
  • アプリに多数のルートがある。

アプリで大規模なルート テーブルの問題が発生しているかどうかを判断する方法

  • 次の 2 つの症状がないかどうかを確認します。
    • 最初の要求でアプリの起動が遅い。
      • これは必須条件ですが、これだけでは不十分です。 ルート以外のさまざまな問題でも、アプリの起動が遅くなることがあります。 以下の条件を確認して、当該の状況がアプリで発生しているかどうかを正確に判断します。
    • アプリが起動時に大量のメモリを消費し、メモリ ダンプに多数の Microsoft.AspNetCore.Routing.Matching.DfaNode インスタンスが示されている。

この問題に対処する方法

ルートに適用可能ないくつかの手法や最適化を使用して、このシナリオを大幅に改善できます。

  • 可能な場合に、{parameter:int}{parameter:guid}{parameter:regex(\\d+)} などのルート制約をパラメーターに適用します。
    • これにより、ルーティング アルゴリズムで、照合に使用される構造を内部的に最適化し、使用メモリを大幅に削減できるようになります。
    • ほとんどの場合、許容できる動作に戻すのにこれで十分です。
  • テンプレート内の後方のセグメントにパラメーターを移動するように、ルートを変更します。
    • これにより、パスの指定されたエンドポイントに一致する可能性がある "パス" の数が減少します。
  • 動的ルートを使用し、コントローラー/ページへのマッピングを動的に実行します。
    • これは、MapDynamicControllerRoute および MapDynamicPageRoute を使用して実施できます。

ルーティング後のミドルウェアのショートサーキット

ルーティングがエンドポイントと一致する場合、通常は、エンドポイントのロジックを呼び出す前にミドルウェア パイプラインの残りの部分を実行できます。 サービスでは、パイプラインの早い段階で既知の要求を除外することで、リソースの使用量を削減できます。 ShortCircuit 拡張メソッドを使って、ルーティングですぐにエンドポイントのロジックを呼び出した後、要求を終了するようにします。 たとえば、ある特定のルートでは認証や CORS ミドルウェアを経由する必要がない場合があります。 次の例では、/short-circuit ルートに一致する要求をショートサーキットします。

app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>) メソッドには、必要に応じて状態コードを指定できます。

MapShortCircuit メソッドを使って、URL プレフィックスの params 配列を渡すことで、複数のルートのショートサーキットを一度に設定します。 たとえば、ブラウザーやボットでは、サーバーで robots.txtfavicon.ico などの既知のパスを調べることがよくあります。 アプリにこれらのファイルがない場合、1 行のコード行で両方のルートを構成できます。

app.MapShortCircuit(404, "robots.txt", "favicon.ico");

MapShortCircuitIEndpointConventionBuilder を返して、ホストのフィルター処理といった追加のルート制約を追加できるようにします。

ShortCircuit メソッドと MapShortCircuit メソッドは、UseRouting の前に配置されたミドルウェアには影響しません。 [Authorize] または [RequireCors] メタデータも持つエンドポイントでこれらのメソッドを使おうとすると、InvalidOperationException で要求が失敗します。 このメタデータは、[Authorize] または [EnableCors] 属性、あるいは RequireCors または RequireAuthorization メソッドによって適用されます。

ミドルウェアをショートサーキットする効果を確認するには、appsettings.Development.json で "Microsoft" のログ カテゴリを "Information" に設定します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

次のコードを実行します。

var app = WebApplication.Create();

app.UseHttpLogging();

app.MapGet("/", () => "No short-circuiting!");
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapShortCircuit(404, "robots.txt", "favicon.ico");

app.Run();

次の例は、/ エンドポイントを実行することで生成されたコンソール ログからのものです。 ログ ミドルウェアからの出力が含まれています。

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Date: Wed, 03 May 2023 21:05:59 GMT
      Server: Kestrel
      Alt-Svc: h3=":5182"; ma=86400
      Transfer-Encoding: chunked

次の例は、/short-circuit エンドポイントを実行した場合のものです。 ミドルウェアがショートサーキットされたため、ログ ミドルウェアからの出力は何も含まれていません。

info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
      The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
      The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.

ライブラリ作成者向けのガイダンス

このセクションでは、ルーティングを基盤とするライブラリ作成者向けのガイダンスを示します。 これらの詳細情報は、アプリの開発者が、ルーティングを拡張するライブラリとフレームワークを使用して優れたエクスペリエンスを実現できるようにすることを目的としています。

エンドポイントを定義する

URL 照合にルーティングを使用するフレームワークを作成するには、まず UseEndpoints の上に構築されるユーザー エクスペリエンスを定義します。

IEndpointRouteBuilder の上に構築します。 これにより、ユーザーは、他の ASP.NET Core 機能と混同せずにフレームワークを構成できます。 すべての ASP.NET Core テンプレートには、ルーティングが含まれます。 ルーティングが存在し、ユーザーになじみのあるものとします。

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

IEndpointConventionBuilder を実装する MapMyFramework(...) の呼び出しから、シールドの具象型を返します。 ほとんどのフレームワーク Map... メソッドは、このパターンに従います。 IEndpointConventionBuilder インターフェイスは、

  • メタデータを構成できます。
  • さまざまな拡張メソッドの対象とされています。

独自の型を宣言すると、独自のフレームワーク固有の機能をビルダーに追加できます。 フレームワークで宣言されたビルダーをラップし、呼び出しを転送するのは問題ありません。

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

独自の EndpointDataSource を作成することを検討しますEndpointDataSource は、エンドポイントのコレクションを宣言および更新するための低レベルのプリミティブです。 EndpointDataSource は、コントローラーと Razor Pages によって使用される強力な API です。 詳細については、「動的エンドポイント ルーティング」を参照してください。

ルーティング テストには、更新されていないデータ ソースの基本的な例があります。

GetGroupedEndpoints を実装することを検討してください。 これにより、グループ化されたエンドポイントで実行されるグループ規則と最終的なメタデータを完全に制御できます。 たとえば、これにより、カスタム EndpointDataSource 実装でグループに追加されたエンドポイント フィルターを実行できます。

既定では、EndpointDataSource の登録を試行しないでくださいUseEndpoints にフレームワークを登録するようユーザーに要求してください。 ルーティングの原理では、既定では何も含まれておらず、UseEndpoints がエンドポイントを登録する場所です。

ルーティング統合ミドルウェアを作成する

メタデータ型をインターフェイスとして定義することを検討します

クラスおよびメソッドの属性としてメタデータ型を使用できるようにします

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

コントローラーや Razor Pages などのフレームワークでは、型およびメソッドへのメタデータ属性の適用がサポートされています。 メタデータ型を宣言する場合:

  • 属性としてアクセスできるようにします。
  • ほとんどのユーザーが属性の適用に精通しています。

メタデータ型をインターフェイスとして宣言すると、柔軟性の高いレイヤーがさらに追加されます。

  • インターフェイスが構成可能です。
  • 開発者は、複数のポリシーを結合する独自の型を宣言できます。

次の例に示すように、メタデータをオーバーライドできるようにします

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

これらのガイドラインに従う最善の方法は、マーカーのメタデータを定義しないようにすることです。

  • メタデータ型が存在するかどうかを確認するだけで終わらせません。
  • メタデータのプロパティを定義し、プロパティを確認します。

メタデータ コレクションが順序付けされ、優先順位によるオーバーライドがサポートされます。 コントローラーの場合、アクション メソッドのメタデータが最も限定的です。

ルーティングを使用する場合もしない場合もミドルウェアが役立つようにします

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

このガイドラインの例として、UseAuthorization ミドルウェアを考えてみましょう。 この承認ミドルウェアを使用すると、フォールバック ポリシーを渡すことができます。 フォールバック ポリシーは、指定されている場合、次の両方に適用されます。

  • 指定されたポリシーのないエンドポイント。
  • エンドポイントに一致しない要求。

これにより、承認ミドルウェアはルーティングのコンテキストの外部で役に立ちます。 承認ミドルウェアは、従来のミドルウェア プログラミングに使用できます。

デバッグ診断

詳細なルーティング診断出力を行うには、Logging:LogLevel:MicrosoftDebug に設定してください。 開発環境では、appsettings.Development.json でログ レベルを次のように設定します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

その他のリソース

ルーティングの役割は、受信した HTTP 要求を照合し、それらの要求をアプリの実行可能なエンドポイントにディスパッチすることです。 エンドポイントは、アプリの実行可能な要求処理コードの単位です。 エンドポイントはアプリで定義され、アプリの起動時に構成されます。 エンドポイントの照合プロセスでは、要求の URL から値を抽出し、それらの値を要求の処理に提供できます。 アプリからのルート情報を使用して、ルーティングでエンドポイントにマップする URL を生成することもできます。

アプリでは、次のものを使用してルーティングを構成できます。

この記事では、ASP.NET Core のルーティングについて詳しく説明します。 ルーティングの構成については、以下を参照してください。

ルーティングの基本

次のコードでは、ルーティングの基本的な例を示します。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前述の例には、MapGet メソッドを使用するエンドポイントが 1 つ含まれます。

  • HTTP GET 要求がルート URL / に送信された場合:
    • 要求デリゲートが実行されます。
    • Hello World! が HTTP 応答に書き込まれます。
  • 要求メソッドが GET ではない場合、またはルート URL が / ではない場合は、一致するルートはなく、HTTP 404 が返されます。

ルーティングでは、UseRoutingUseEndpoints によって登録されたミドルウェアのペアを使用します。

  • UseRouting では、ルートの照合がミドルウェア パイプラインに追加されます。 このミドルウェアによって、アプリで定義されているエンドポイントのセットが調べられ、要求に基づいて最適な一致が選択されます。
  • UseEndpoints では、エンドポイントの実行がミドルウェア パイプラインに追加されます。 選択されたエンドポイントに関連付けられているデリゲートが実行されます。

通常は、アプリで UseRouting または UseEndpoints を呼び出す必要はありません。 WebApplicationBuilder は、Program.cs に追加されたミドルウェアを UseRoutingUseEndpoints でラップするミドルウェア パイプラインを構成します。 ただし、アプリでは、これらのメソッドを明示的に呼び出すことによって、UseRoutingUseEndpoints の実行順序を変更できます。 たとえば、次のコードでは、UseRouting を明示的に呼び出しています。

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

上のコードでは以下の操作が行われます。

  • app.Use の呼び出しにより、パイプラインの開始時に実行されるカスタム ミドルウェアが登録されます。
  • UseRouting の呼び出しにより、ルートの照合ミドルウェアがカスタム ミドルウェアの "後に" 実行されるように構成されます。
  • MapGet に登録されたエンドポイントが、パイプラインの最後で実行されます。

前述の例に UseRouting の呼び出しが含まれていなかった場合、カスタム ミドルウェアは、ルートの照合ミドルウェアの "後に" 実行されます。

エンドポイント

エンドポイントを定義するには、MapGet メソッドが使用されます。 エンドポイントとは、次のようなものです。

  • URL と HTTP メソッドを一致させることによって選択できます。
  • デリゲートを実行することによって実行できます。

アプリによって一致させて実行できるエンドポイントは、UseEndpoints で構成します。 たとえば、MapGetMapPost、および類似のメソッドでは、要求のデリゲートがルーティング システムに接続されます。 他のメソッドを使用して、ASP.NET Core フレームワークの機能をルーティング システムに接続できます。

次の例では、より高度なルート テンプレートによるルーティングを示します。

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

文字列 /hello/{name:alpha} は、ルート テンプレートです。 ルート テンプレートは、エンドポイントの一致方法を構成するために使用されます。 この場合、テンプレートは次のものと一致します。

  • /hello/Docs のような URL
  • /hello/ で始まり、その後に一連の英字が続く任意の URL パス。 :alpha では、英字のみと一致するルート制約が適用されます。 ルート制約については、この記事で後ほど説明します。

URL パスの 2 番目のセグメント {name:alpha} は次のようになります。

  • name パラメーターにバインドされます。
  • キャプチャされて HttpRequest.RouteValues に格納されます。

次の例では、正常性チェックと承認を使用するルーティングを示します。

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

上の例では、次の方法が示されています。

  • ルーティングで承認ミドルウェアを使用できます。
  • エンドポイントを使用して、承認動作を構成できます。

MapHealthChecks の呼び出しにより、正常性チェック エンドポイントが追加されます。 この呼び出しに RequireAuthorization をチェーンすると、エンドポイントに承認ポリシーがアタッチされます。

UseAuthenticationUseAuthorization を呼び出すと、認証ミドルウェアと承認ミドルウェアが追加されます。 これらのミドルウェアは、次のことができるように UseRoutingUseEndpoints の間に配置されます。

  • UseRouting によって選択されたエンドポイントを確認します。
  • UseEndpoints によってエンドポイントにディスパッチされる前に、承認ポリシーを適用します。

エンドポイントのメタデータ

前の例には 2 つのエンドポイントがありますが、承認ポリシーがアタッチされているのは正常性チェック エンドポイントだけです。 要求が正常性チェック エンドポイント /healthz と一致した場合、承認チェックが実行されます。 これは、エンドポイントに追加のデータをアタッチできることを示しています。 この追加データは、エンドポイントのメタデータと呼ばれます。

  • メタデータは、ルーティング対応ミドルウェアによって処理できます。
  • メタデータには、任意の .NET 型を使用できます。

ルーティングの概念

ルーティング システムは、ミドルウェア パイプラインを基にして、強力なエンドポイント概念を追加することにより、構築されています。 エンドポイントは、ルーティング、承認、および任意の数の ASP.NET Core システムに関して相互に独立している、アプリの機能の単位を表します。

ASP.NET Core エンドポイントの定義

ASP.NET Core エンドポイントとは次のようなものです。

  • 実行可能: RequestDelegate を持っています。
  • 拡張可能: Metadata コレクションを持っています。
  • Selectable: 必要に応じて、ルーティング情報を持ちます。
  • 列挙可能: エンドポイントのコレクションの一覧は、DI から EndpointDataSource を取得することによって得られます。

次のコードでは、エンドポイントを取得し、現在の要求と一致するものを検査する方法を示します。

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

エンドポイントが選択されている場合は、HttpContext から取得できます。 そのプロパティを検査できます。 エンドポイント オブジェクトは不変であり、作成後に変更することはできません。 最も一般的なエンドポイントの型は RouteEndpoint です。 RouteEndpoint には、ルーティング システムによって選択されるための情報が含まれています。

前述のコードでは、app.Use によってインライン ミドルウェアが構成されます。

次のコードでは、パイプラインで app.Use が呼び出される場所によっては、エンドポイントが存在しない場合があることを示します。

// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

前述のサンプルでは、エンドポイントが選択されているかどうかを表示する Console.WriteLine ステートメントが追加されています。 わかりやすくするため、このサンプルでは、指定された / エンドポイントに表示名が割り当てられています。

前述のサンプルには、これらのミドルウェアがパイプライン内で実行されるタイミングを正確に制御するための UseRouting および UseEndpoints の呼び出しも含まれています。

このコードを / の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

このコード他の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

この出力は次のことを示しています。

  • UseRouting が呼び出される前は、エンドポイントは常に null になっています。
  • 一致が見つかった場合、エンドポイントは UseRoutingUseEndpoints の間で null 以外の値になります。
  • 一致が見つかると、UseEndpoints ミドルウェアはターミナルです。 ターミナル ミドルウェアについては、この記事で後ほど定義します。
  • UseEndpoints の後のミドルウェアは、一致が検出されなかった場合にのみ実行されます。

UseRouting ミドルウェアでは、SetEndpoint メソッドを使用して、エンドポイントが現在のコンテキストにアタッチされます。 UseRouting ミドルウェアをカスタム ロジックに置き換えることができ、その場合でもエンドポイントを使用する利点を得られます。 エンドポイントはミドルウェアのような低レベルのプリミティブであり、ルーティングの実装には結合されません。 ほとんどのアプリでは、UseRouting をカスタム ロジックに置き換える必要はありません。

UseEndpoints ミドルウェアは、UseRouting ミドルウェアと連携して使用するように設計されています。 エンドポイントを実行するためのコア ロジックは複雑ではありません。 GetEndpoint を使用してエンドポイントを取得し、その RequestDelegate プロパティを呼び出します。

次のコードでは、ミドルウェアがルーティングに与える影響またはルーティングに対応する方法を示します。

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

前の例では、2 つの重要な概念が示されています。

  • ミドルウェアは、UseRouting の前に実行して、ルーティングの動作に使用されるデータを変更できます。
  • ミドルウェアは、UseRoutingUseEndpoints の間で実行して、エンドポイントが実行される前にルーティングの結果を処理できます。
    • UseRoutingUseEndpoints の間で実行されるミドルウェア:
      • 通常、メタデータを検査してエンドポイントを認識します。
      • 多くの場合、UseAuthorizationUseCors によって実行されるセキュリティに関する決定を行います。
    • ミドルウェアとメタデータを組み合わせることで、エンドポイントごとにポリシーを構成できます。

上のコードでは、エンドポイントごとのポリシーをサポートするカスタム ミドルウェアの例が示されています。 ミドルウェアによって、機密データへのアクセスの "監査ログ" がコンソールに書き込まれます。 RequiresAuditAttribute メタデータを使用して、エンドポイントを "監査する" ようにミドルウェアを構成できます。 このサンプルでは、機密としてマークされているエンドポイントのみが監査される "オプトイン" パターンを示します。 このロジックの逆を定義して、たとえば安全としてマークされていないすべてのものを監査することができます。 エンドポイント メタデータ システムは柔軟です。 ユース ケースに適したどのような方法でも、このロジックを設計できます。

前のサンプル コードは、エンドポイントの基本的な概念を示すことが意図されています。 サンプルは運用環境での使用は意図されていません。 より完全なバージョンの "監査ログ" ミドルウェアでは、次のことが行われます。

  • ファイルまたはデータベースにログを記録します。
  • ユーザー、IP アドレス、機密性の高いエンドポイントの名前などの詳細情報が追加されます。

コントローラーや SignalR などのクラスベースのフレームワークで簡単に使用できるように、監査ポリシー メタデータ RequiresAuditAttributeAttribute と定義されています。 "コードへのルーティング" を使用すると、次のようになります。

  • メタデータがビルダー API にアタッチされます。
  • エンドポイントを作成するとき、クラス ベースのフレームワークに、対応するメソッドとクラスのすべての属性が組み込まれます。

メタデータの型に対するベスト プラクティスは、インターフェイスまたは属性として定義することです。 インターフェイスと属性では、コードを再利用できます。 メタデータ システムは柔軟であり、どのような制限もありません。

ターミナル ミドルウェアとルーティングを比較する

次の例は、ターミナル ミドルウェアとルーティングの両方を示しています。

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Approach 1: で示されているミドルウェアのスタイルは、ターミナル ミドルウェアです。 ターミナル ミドルウェアと呼ばれるのは、照合操作を実行するためです。

  • 前のサンプルの照合操作は、ミドルウェアの場合は Path == "/"、ルーティングの場合は Path == "/Routing" です。
  • 照合が成功すると、next ミドルウェアを呼び出すのではなく、一部の機能を実行して戻ります。

ターミナル ミドルウェアと呼ばれるのは、検索を終了し、いくつかの機能を実行してから制御を返すためです。

次の一覧では、ターミナル ミドルウェアとルーティングを比較しています。

  • どちらの方法でも、処理パイプラインを終了できます。
    • ミドルウェアでは、next を呼び出すのではなく、戻ることによってパイプラインが終了されます。
    • エンドポイントは常にターミナルです。
  • ターミナル ミドルウェアを使用すると、パイプライン内の任意の場所にミドルウェアを配置できます。
    • エンドポイントは、UseEndpoints の位置で実行されます。
  • ターミナル ミドルウェアでは、任意のコードを使用してミドルウェアが一致するかどうかを判定できます。
    • カスタム ルート一致コードは、冗長で、正しく記述するのが困難な場合があります。
    • ルーティングでは、一般的なアプリに対して簡単なソリューションが提供されます。 ほとんどのアプリでは、カスタム ルート一致コードは必要ありません。
  • UseAuthorizationUseCors などのミドルウェアを使用したエンドポイント インターフェイス。
    • UseAuthorization または UseCors でターミナル ミドルウェアを使用するには、承認システムとの手動インターフェイスが必要です。

エンドポイント では、次の両方が定義されます。

  • 要求を処理するためのデリゲート。
  • 任意のメタデータのコレクション。 メタデータは、各エンドポイントにアタッチされている構成とポリシーに基づいて横断的な関心事を実装するために使用されます。

ターミナル ミドルウェアは効果的なツールになる可能性がありますが、次のものが必要です。

  • 大量のコーディングとテスト。
  • 必要なレベルの柔軟性を実現するための、他のシステムとの手作業による統合。

ターミナル ミドルウェアを作成する前に、ルーティングとの統合を検討してください。

Map または MapWhen と統合されている既存のターミナル ミドルウェアは、通常、ルーティング対応のエンドポイントにすることができます。 MapHealthChecks では、ルーターウェアのパターンが示されています。

  • IEndpointRouteBuilder で拡張メソッドを作成します。
  • CreateApplicationBuilder を使用して、入れ子になったミドルウェア パイプラインを作成します。
  • ミドルウェアを新しいパイプラインにアタッチします。 例では、 UseHealthChecksが使用されます。
  • RequestDelegate にミドルウェア パイプラインを Build します。
  • Map を呼び出し、新しいミドルウェア パイプラインを提供します。
  • 拡張メソッドから Map によって提供されるビルダー オブジェクトを返します。

次のコードでは、MapHealthChecks の使用方法を示します。

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

前のサンプルでは、ビルダー オブジェクトを返すことが重要である理由が示されています。 ビルダー オブジェクトを返すことで、アプリ開発者はエンドポイントの承認などのポリシーを構成できます。 この例では、正常性チェック ミドルウェアと承認システムは直接統合されていません。

そのメタデータ システムは、ターミナル ミドルウェアを使用する機能拡張作成者によって、発生する問題に対応して作成されました。 各ミドルウェアで承認システムとの独自の統合を実装することには問題があります。

URL 一致

  • ルーティングによって受信要求がエンドポイントと照合されるプロセスです。
  • URL パスとヘッダーのデータに基づいています。
  • 要求内の任意のデータを考慮するように拡張できます。

実行されたルーティング ミドルウェアでは、Endpoint が設定され、現在の要求からの HttpContext要求機能に値がルーティングされます。

  • HttpContext.GetEndpoint を呼び出すと、エンドポイントが取得されます。
  • HttpRequest.RouteValues では、ルート値のコレクションが取得されます。

ルーティング ミドルウェアの後で実行されたミドルウェアでは、エンドポイントを調べて、アクションを実行することができます。 たとえば、承認ミドルウェアでは、エンドポイントのメタデータ コレクションに対し、承認ポリシーを問い合わせることができます。 要求処理パイプライン内のすべてのミドルウェアが実行された後、選択したエンドポイントのデリゲートが呼び出されます。

エンドポイント ルーティングのルーティング システムでは、配布に関するすべての決定が行われます。 ミドルウェアでは選択されたエンドポイントに基づいてポリシーが適用されるため、次のことが重要です。

  • ディスパッチまたはセキュリティ ポリシーの適用に影響を与える可能性のある決定は、ルーティング システム内で行われます。

警告

後方互換性のために、コントローラーまたは Razor Pages エンドポイント デリゲートが実行されると、それまでに実行された要求処理に基づいて、RouteContext.RouteData のプロパティが適切な値に設定されます。

RouteContext の種類は、今後のリリースでは古いものとしてマークされます。

  • RouteData.ValuesHttpRequest.RouteValues に移行します。
  • RouteData.DataTokens を移行して、エンドポイント メタデータから IDataTokensMetadata を取得します。

URL の照合は、構成可能な一連のフェーズで動作します。 各フェーズでの出力は一致のセットとなります。 一致のセットは、次のフェーズでさらに絞り込むことができます。 ルーティングの実装では、一致するエンドポイントの処理順序は保証されません。 一致の可能性のあるものは一度にすべて処理されます。 URL 照合フェーズは、次の順序で発生します。 ASP.NET Core:

  1. エンドポイントのセットおよびそれらのルート テンプレートに対して URL パスを処理し、すべての一致を収集します。
  2. 前のリストを取得し、ルート制約が適用されると失敗する一致を削除します。
  3. 前のリストを取得し、MatcherPolicy インスタンスのセットを失敗させる一致を削除します。
  4. EndpointSelector を使用して、前のリストから最終的な決定を行います。

エンドポイントのリストは、次の内容に従って優先度付けが行われます。

EndpointSelector に到達するまで、各フェーズで一致するすべてのエンドポイントが処理されます。 EndpointSelector は最後のフェーズです。 一致の中から最も優先度の高いエンドポイントが最適な一致として選択されます。 最適な一致と優先度が同じである一致が他にもある場合は、あいまい一致の例外がスローされます。

ルートの優先順位はより具体的なルート テンプレートに、より高い優先度が与えられることに基づいて算出されます。 たとえば、テンプレート /hello/{message} を検討してみます。

  • どちらも URL パス /hello と一致します。
  • /hello の方がより具体的であるため、優先度が高くなります。

一般に、ルートの優先順位は、実際に使用される URL スキームの種類として最適なものを選択するのに適しています。 Order は、あいまいさを避けるために必要な場合にのみ使用します。

拡張性の種類がルーティングによって指定されるため、あいまいなルートを事前にルーティング システムによって計算することはできません。 ルート テンプレート /{message:alpha}/{message:int} などの例を考えてみましょう。

  • alpha 制約を使用すると、アルファベット文字のみと一致します。
  • int 制約を使用すると、数値のみと一致します。
  • これらのテンプレートのルート優先順位は同じですが、この両方と一致する単一の URL はありません。
  • 起動時にルーティング システムからあいまいエラーが報告された場合、それによってこの有効なユース ケースはブロックされます。

警告

UseEndpoints 内での処理の順序は、ルーティングの動作には影響しませんが、例外が 1 つあります。 MapControllerRoute および MapAreaRoute では、それぞれが呼び出された順序に基づいて、それぞれのエンドポイントに順序値が自動的に割り当てられます。 これにより、ルーティング システムでより古いルーティング実装と同じ保証を提供しなくても、コントローラーの長時間の動作がシミュレートされます。

ASP.NET Core でのエンドポイントのルーティングは、

  • ルートの概念がありません。
  • 順序付けが保証されません。 すべてのエンドポイントが一度に処理されます。

ルート テンプレートの優先順位とエンドポイントの選択順序

ルート テンプレートの優先順位とは、どれほど具体的であるかに基づいて、各ルート テンプレートに値を割り当てるシステムです。 ルート テンプレートの優先順位:

  • 一般的なケースでは、エンドポイントの順序を調整する必要はなくなります。
  • 一般的に期待されるルーティング動作との一致が試みられます。

たとえば、テンプレート /Products/List/Products/{id} について考えてみます。 URL パス /Products/List に対しては、/Products/List の方が /Products/{id} よりも適していると想定するのが妥当です。 このように言えるのは、リテラル セグメント /List がパラメーター セグメント /{id} よりも優先順位が高いと見なされるためです。

優先順位のしくみの詳細は、ルート テンプレートの定義方法と関連付けられています。

  • より多くのセグメントを持つテンプレートは、より具体的なものと見なされます。
  • リテラル テキストを含むセグメントは、パラメーター セグメントよりも具体的であると見なされます。
  • 制約が含まれるパラメーター セグメントは、それが含まれないものよりも具体的であると見なされます。
  • 複雑なセグメントは、制約を含むパラメーター セグメントと同じくらい具体的であると見なされます。
  • キャッチオール パラメーターは、まったく具体的ではありません。 キャッチオール ルートに関する重要な情報については、「ルート テンプレート」セクションのキャッチオールに関する記述を参照してください。

URL 生成の概念

URL の生成:

  • ルーティングにおいて、一連のルート値に基づいて URL パスを作成するプロセスです。
  • エンドポイントとそれにアクセスする URL を論理的に分離できます。

エンドポイント ルーティングには、LinkGenerator API が含まれます。 LinkGeneratorDI から使用できるシングルトン サービスです。 LinkGenerator API は、実行中の要求のコンテキスト外で使用することができます。 Mvc.IUrlHelper と、タグ ヘルパー、HTML ヘルパー、アクション結果など、IUrlHelper に依存するシナリオでは LinkGenerator API を内部的に使用して、リンク生成機能が提供されます。

リンク ジェネレーターは、アドレスアドレス スキーム の概念に基づいています。 アドレス スキームは、リンク生成で考慮すべきエンドポイントを決定する方法です。 たとえば、コントローラーおよび Razor Pages からの、多くのユーザーに馴染みのあるルート名やルート値シナリオは、アドレス スキームとして実装されます。

リンク ジェネレーターでは、次の拡張メソッドを介して、コントローラーおよび Razor Pages にリンクできます。

これらのメソッドのオーバーロードでは、HttpContext を含む引数が受け入れられます。 これらのメソッドは Url.Action および Url.Page と機能的には同等ですが、柔軟性とオプションがさらに提供されます。

GetPath* メソッドは、絶対パスを含む URI を生成するという点で Url.Action および Url.Page に最も似ています。 GetUri* メソッドでは常に、スキームとホストを含む絶対 URI が生成されます。 HttpContext を受け入れるメソッドでは、実行中の要求のコンテキストで URI が生成されます。 実行中の要求からのアンビエント ルート値、URL ベース パス、スキーム、およびホストは、オーバーライドされない限り使用されます。

LinkGenerator はアドレスと共に呼び出されます。 URI の生成は、次の 2 つの手順で行われます。

  1. アドレスは、そのアドレスと一致するエンドポイントのリストにバインドされます。
  2. 各エンドポイントの RoutePattern は、指定された値と一致するルート パターンが見つかるまで評価されます。 結果の出力は、リンク ジェネレーターに指定された他の URI 部分と結合され、返されます。

LinkGenerator によって提供されるメソッドでは、すべての種類のアドレスの標準的なリンク生成機能がサポートされます。 リンク ジェネレーターを使用する最も便利な方法は、特定のアドレスの種類の操作を実行する拡張メソッドを使用することです。

拡張メソッド 説明
GetPathByAddress 指定された値に基づき、絶対パスを含む URI を生成します。
GetUriByAddress 指定された値に基づき、絶対 URI を生成します。

警告

LinkGenerator メソッド呼び出しによる次の影響に注意してください。

  • 受信要求の Host ヘッダーが確認されないアプリ構成では、GetUri* 拡張メソッドは注意して使用してください。 受信要求の Host ヘッダーが確認されていない場合、信頼されていない要求入力を、ビューまたはページの URI でクライアントに送り返すことができます。 すべての運用アプリで、Host ヘッダーを既知の有効な値と照らし合わせて確認するようにサーバーを構成することをお勧めします。

  • ミドルウェアで Map または MapWhen と組み合わせて、LinkGenerator を使用する場合は注意してください。 Map* では、実行中の要求の基本パスが変更され、リンク生成の出力に影響します。 すべての LinkGenerator API で基本パスを指定することができます。 リンク生成への Map* の影響を元に戻すための空の基本パスを指定してください。

ミドルウェアの例

次の例では、ミドルウェアで LinkGenerator API を使用して、商品をリストするアクション メソッドへのリンクを作成します。 リンク ジェネレーターは、クラスに挿入し、GenerateLink を呼び出すことで、アプリのどのクラスでも使用できます。

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

ルート テンプレート

{} 内のトークンでは、ルートが一致した場合にバインドされるルート パラメーターが定義されます。 1 つのルート セグメントに複数のルート パラメーターを定義できますが、各ルート パラメーターをリテラル値で区切る必要があります。 次に例を示します。

{controller=Home}{action=Index}

これは、{controller}{action} の間にリテラル値がないため、有効なルートではありません。 ルート パラメーターには名前を付ける必要があります。付加的な属性を指定することもあります。

ルート パラメーター以外のリテラル テキスト ({id} など) とパス区切り / は URL のテキストに一致する必要があります。 テキスト照合は復号された URL のパスを基盤とし、大文字と小文字が区別されます。 リテラル ルート パラメーターの区切り記号 ({ または }) を照合するには、文字を繰り返して区切り記号をエスケープします。 たとえば、{{ または }} です。

アスタリスク * または二重アスタリスク**:

  • ルート パラメーターのプレフィックスとして使用して、URI の残りの部分にバインドすることができます。
  • キャッチオール パラメーターと呼ばれています。 blog/{**slug} の例を次に示します。
    • blog/ で始まり、その後に任意の値が続く URI と一致します。
    • blog/ に続く値は、slug ルート値に割り当てられます。

警告

ルーティングでバグが原因で、キャッチオール パラメーターがルートと正しく一致しない可能性があります。 このバグの影響を受けるアプリには、次の特性があります。

  • キャッチオール ルート (たとえば、{**slug}")
  • キャッチオール ルートが、一致すべき要求と一致しません。
  • 他のルートを削除すると、キャッチオール ルートが機能し始めます。

このバグが発生するケースの例については、GitHub のバグ 18677 および 16579 を参照してください。

このバグのオプトイン修正は .NET Core 3.1.301 SDK 以降に含まれています。 次のコードにより、このバグを修正する内部スイッチが設定されます。

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

キャッチオール パラメーターは空の文字列に一致することもあります。

キャッチオール パラメーターでは、パス区切り / 文字を含め、URL の生成にルートが使用されるときに適切な文字がエスケープされます。 たとえば、ルート値が { path = "my/path" } のルート foo/{*path} では、foo/my%2Fpath が生成されます。 エスケープされたスラッシュに注意してください。 パス区切り文字をラウンドトリップさせるには、** ルート パラメーター プレフィックスを使用します。 { path = "my/path" } のルート foo/{**path} では、foo/my/path が生成されます。

任意のファイル拡張子が付いたファイル名のキャプチャを試行する URL パターンには、追加の考慮事項があります。 たとえば、テンプレート files/{filename}.{ext?} について考えてみます。 filenameext の両方の値が存在するときに、両方の値が入力されます。 URL に filename の値だけが存在する場合、末尾の . は任意であるため、このルートは一致となります。 次の URL はこのルートに一致します。

  • /files/myFile.txt
  • /files/myFile

ルート パラメーターには、既定値 が含まれることがあります。パラメーター名の後に既定値を指定し、等号 (=) で区切ることで指定されます。 たとえば、{controller=Home} では、controller の既定値として Home が定義されます。 パラメーターの URL に値がない場合、既定値が使用されます。 ルート パラメーターは、パラメーター名の終わりに疑問符 (?) を追加することでオプションとして扱われます。 たとえば、id? のようにします。 省略可能な値と既定のルート パラメーターの違いは次のとおりです。

  • 既定値を持つルート パラメーターでは常に値が生成されます。
  • 省略可能なパラメーターには、要求 URL によって値が指定された場合にのみ値が含められます。

ルート パラメーターには、URL からバインドされるルート値に一致しなければならないという制約が含まれることがあります。 : と制約名をルート パラメーター名の後に追加すると、ルート パラメーターのインライン制約が指定されます。 その制約で引数が要求される場合、制約名の後にかっこ (...) で囲まれます。 複数の "インライン制約" を指定するには、別の : と制約名を追加します。

制約名と引数が IInlineConstraintResolver サービスに渡され、URL 処理で使用する IRouteConstraint のインスタンスが作成されます。 たとえば、ルート テンプレート blog/{article:minlength(10)} によって、制約 minlength と引数 10 が指定されます。 ルート制約の詳細とこのフレームワークによって指定される制約のリストについては、「ルート制約」セクションを参照してください。

ルート パラメーターには、パラメーター トランスフォーマーを指定することもできます。 パラメーター トランスフォーマーを指定すると、リンクを生成し、アクションおよびページを URL と一致させるときにパラメーターの値が変換されます。 制約と同様に、パラメーター トランスフォーマーをルート パラメーターにインラインで追加することができます。その場合、ルート パラメーター名の後に : とトランスフォーマー名を追加します。 たとえば、ルート テンプレート blog/{article:slugify} では、slugify トランスフォーマーが指定されます。 パラメーター トランスフォーマーの詳細については、「パラメーター トランスフォーマー」セクションを参照してください。

次の表に、ルート テンプレートの例とその動作を示します。

ルート テンプレート 一致する URI の例 要求 URI
hello /hello 単一パス /hello にのみ一致します。
{Page=Home} / 一致し、PageHome に設定されます。
{Page=Home} /Contact 一致し、PageContact に設定されます。
{controller}/{action}/{id?} /Products/List Products コントローラーと List アクションにマッピングされます。
{controller}/{action}/{id?} /Products/Details/123 Products コントローラーと Details アクションにマッピングされ、id は 123 に設定されます。
{controller=Home}/{action=Index}/{id?} / Home コントローラーと Index メソッドにマッピングされます。 id は無視されます。
{controller=Home}/{action=Index}/{id?} /Products Products コントローラーと Index メソッドにマッピングされます。 id は無視されます。

一般的に、テンプレートの利用が最も簡単なルーティングの手法となります。 ルート テンプレート以外では、制約と既定値も指定できます。

複雑なセグメント

複雑なセグメントは、リテラル区切り文字を右から左に最短一致の方法で照合することによって処理されます。 たとえば、[Route("/a{b}c{d}")] は複雑なセグメントです。 複雑なセグメントは、それらを適切に使用する上で理解する必要がある特定の方法で機能します。 このセクションの例では、パラメーター値の中に区切り文字が含まれていない場合にのみ、複雑なセグメントが本当にうまく機能する理由を示します。 より複雑なケースでは、regex を使用し、値を手動で抽出する必要があります。

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

これは、ルーティングがテンプレート /a{b}c{d} と URL パス /abcd を使用して実行するステップの概要です。 | は、アルゴリズムの動作を視覚化するために使用されます。

  • 最初のリテラル (右から左へ) は c です。 そこで、/abcd は右から検索され、/ab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /ab|c|d は中断したところから検索されて、a が見つかり、/|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • 残りのテキストも残りのルート テンプレートも存在しないため、これは一致となります。

同じテンプレート /a{b}c{d} と、URL パス /aabcd を使用した場合の否定の例を次に示します。 | は、アルゴリズムの動作を視覚化するために使用されます。 このケースは一致ではありませんが、同じアルゴリズムで説明します。

  • 最初のリテラル (右から左へ) は c です。 そこで、/aabcd は右から検索され、/aab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /aab|c|d は中断したところから検索されて、a が見つかり、/a|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • この時点で、テキスト a が残っていますが、アルゴリズムは解析するためのルート テンプレートを使い果たしたので、これは一致とはなりません。

照合アルゴリズムは最短一致のため、次のようになります。

  • 各ステップで可能な限りの最短のテキストに一致します。
  • パラメーター値の内部に区切り記号の値が表示されている場合は一致しません。

正規表現を使用すると、一致の動作をより細かく制御できます。

最長一致 (遅延一致 とも呼ばれる) を使用すると、可能な限り長い文字列と一致します。 最短一致は、可能な限り最短の文字列と一致します。

特殊文字を含むルーティング

特殊文字を含むルーティングは、予期しない結果になる可能性があります。 たとえば、次のアクション メソッドを含むコントローラーについて考えます。

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

string id に次のエンコードされた値が含まれる場合、予期しない結果が発生する可能性があります。

ASCII Encoded
/ %2F
+

ルート パラメーターは常に URL でデコードされるとは限りません。 この問題は今後対処される可能性があります。 詳しくは、こちらの GitHub イシューに関するページをご覧ください。

ルート制約

ルート制約は、受信 URL と一致し、URL パスがルート値にトークン化されたときに実行されます。 ルート制約では、通常、ルート テンプレート経由で関連付けられるルート値を調べ、値が許容できるかどうかを true または false で決定します。 一部のルート制約では、ルート値以外のデータを使用し、要求をルーティングできるかどうかが考慮されます。 たとえば、HttpMethodRouteConstraint はその HTTP Verb に基づいて要求を承認または却下します。 制約は、要求のルーティングとリンクの生成で使用されます。

警告

入力の検証には制約を使用しないでください。 入力の検証に制約を使用した場合、入力が無効だと "404 見つかりません" が返されます。 無効な入力の場合は、"400 要求が無効です" と適切なエラー メッセージが生成されます。 ルート制約は、特定のルートに対する入力の妥当性を検証するためではなく、似たようなルートの違いを明らかにするために使用されます。

次の表では、ルート制約の例とそれに求められる動作をまとめています。

制約 一致の例 メモ
int {id:int} 123456789-123456789 あらゆる整数に一致する
bool {active:bool} trueFALSE true または false に一致する。 大文字と小文字は区別されない
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm インバリアント カルチャの有効な DateTime 値に一致します。 前の警告を参照してください。
decimal {price:decimal} 49.99-1,000.01 インバリアント カルチャの有効な decimal 値に一致します。 前の警告を参照してください。
double {weight:double} 1.234-1,001.01e8 インバリアント カルチャの有効な double 値に一致します。 前の警告を参照してください。
float {weight:float} 1.234-1,001.01e8 インバリアント カルチャの有効な float 値に一致します。 前の警告を参照してください。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 有効な Guid 値に一致する
long {ticks:long} 123456789-123456789 有効な long 値に一致する
minlength(value) {username:minlength(4)} Rick 4 文字以上の文字列であることが必要
maxlength(value) {filename:maxlength(8)} MyFile 8 文字以内の文字列であることが必要
length(length) {filename:length(12)} somefile.txt 厳密に 12 文字の文字列であることが必要
length(min,max) {filename:length(8,16)} somefile.txt 8 文字以上、16 文字以内の文字列であることが必要
min(value) {age:min(18)} 19 18 以上の整数値であることが必要
max(value) {age:max(120)} 91 120 以下の整数値であることが必要
range(min,max) {age:range(18,120)} 91 18 以上、120 以下の整数値であることが必要
alpha {name:alpha} Rick 文字列は 1 つまたは複数のアルファベット文字で構成されることが必要 (a-z、大文字と小文字は区別されません)。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 文字列は正規表現と一致する必要があります。 正規表現の定義に関するヒントを参照してください。
required {name:required} Rick URL 生成中、非パラメーターが提示されるように強制する

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

1 のパラメーターには、複数の制約をコロンで区切って適用できます。 たとえば、次の制約では、パラメーターが 1 以上の整数値に制限されます。

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

警告

URL の妥当性を検証し、CLR 型に変換するルート制約では、常にインバリアント カルチャが使用されます。 たとえば、int または DateTime の CLR 型に変換される場合などです。 これらの制約では、URL のローカライズが不可であることが前提です。 フレームワークから提供されるルート制約がルート値に格納されている値を変更することはありません。 URL から解析されたルート値はすべて文字列として格納されます。 たとえば、float 制約はルート値を浮動小数に変換しますが、変換された値は、浮動小数に変換できることを検証するためにだけ利用されます。

正規表現の制約

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

正規表現は、regex(...) ルート制約を使用して、インライン制約として指定できます。 MapControllerRoute ファミリのメソッドでも、制約のオブジェクト リテラルを取ります。 この形式が使用されている場合、文字列値は正規表現として解釈されます。

次のコードでは、インラインで regex 制約が使用されています。

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

次のコードでは、regex 制約の指定にオブジェクト リテラルが使用されています。

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

ASP.NET Core フレームワークでは、正規表現コンストラクターに RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant が追加されます。 これらのメンバーの詳細については、「RegexOptions」を参照してください。

正規表現では、ルーティングや C# 言語で使用されるものに似た区切り記号とトークンが使用されます。 正規表現トークンはエスケープする必要があります。 インライン制約で正規表現 ^\d{3}-\d{2}-\d{4}$ を使用するには、次のいずれかを使用します。

  • \ 文字列エスケープ文字をエスケープするには、文字列で指定した \ 文字を、C# ソース ファイル内の \\ 文字に置き換えます。
  • 逐語的文字列リテラル

ルーティング パラメーター区切り記号文字 ({}[]) をエスケープするには、表現の文字を二重にします (例: {{}}[[]])。 次の表に、正規表現とそれにエスケープを適用した後のものを示します。

正規表現 エスケープ適用後の正規表現
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

ルーティングで使用される正規表現は、多くの場合、^ 文字で始まり、これは文字列の開始位置と一致します。 この式は、多くの場合、$ 文字で終わり、文字列の末尾と一致します。 ^ 文字と $ 文字により、正規表現がルート パラメーター値全体に一致することが保証されます。 ^ 文字と $ 文字がなければ、意図に反し、正規表現は文字列内のあらゆる部分文字列に一致してしまいます。 下の表では、一致または不一致の理由を例を示し説明します。

正規表現 String 一致したもの コメント
[a-z]{2} hello はい サブ文字列の一致
[a-z]{2} 123abc456 はい サブ文字列の一致
[a-z]{2} mz はい 一致する表現
[a-z]{2} MZ はい 大文字と小文字の使い方が違う
^[a-z]{2}$ hello いいえ 上の ^$ を参照
^[a-z]{2}$ 123abc456 いいえ 上の ^$ を参照

正規表現構文の詳細については、.NET Framework 正規表現に関するページを参照してください。

既知の入力可能値の集まりにパラメーターを制限するには、正規表現を使用します。 たとえば、{action:regex(^(list|get|create)$)} の場合、action ルート値は listgetcreate とのみ照合されます。 制約ディクショナリに渡された場合、文字列 ^(list|get|create)$ で同じものになります。 既知の制約に一致しない、制約ディクショナリに渡された制約も、正規表現として扱われます。 既知の制約に一致しない、テンプレート内で渡される制約は、正規表現としては扱われません。

カスタム ルート制約

カスタム ルート制約は、IRouteConstraint インターフェイスを実装して作成できます。 IRouteConstraint インターフェイスには、Match が含まれています。これでは、制約が満たされている場合は true を返し、それ以外の場合は false を返します。

カスタム ルート制約は通常必要ありません。 カスタム ルート制約を実装する前に、モデル バインドなどの代替手段を検討してください。

ASP.NET Core の Constraints フォルダーには、制約を作成するための適切な例が用意されています。 たとえば、GuidRouteConstraint です。

カスタムの IRouteConstraint を使うには、サービス コンテナー内の ConstraintMap に、ルート制約の種類が登録されている必要があります。 ConstraintMap は、ルート制約キーを、その制約を検証する IRouteConstraint の実装にマッピングするディクショナリです。 アプリの ConstraintMap は、Program.cs で、AddRouting 呼び出しの一部として、または builder.Services.Configure<RouteOptions> を使って RouteOptions を直接構成することで、更新できます。 次に例を示します。

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

上記の制約は、次のコードに適用されます。

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

NoZeroesRouteConstraint を実装することにより、ルート パラメーターで 0 が使用されるのを回避できます。

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

上記のコードでは次の操作が行われます。

  • ルートの {id} セグメントの 0 を禁止します。
  • カスタム制約を実装する基本的な例を示しています。 実稼働しているアプリでは使用しないでください。

次のコードは、0 を含む id が処理されないようにする優れた方法です。

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

上記のコードには NoZeroesRouteConstraint アプローチに対し、次の利点があります。

  • カスタム制約が必要ありません。
  • ルート パラメーターに 0 が含まれている場合は、よりわかりやすいエラーが返されます。

パラメーター トランスフォーマー

パラメーター トランスフォーマー:

たとえば、Url.Action(new { article = "MyTestArticle" }) のルート パターン blog\{article:slugify} のカスタム slugify パラメーター トランスフォーマーでは、blog\my-test-article が生成されます。

IOutboundParameterTransformer の次の実装を見てみましょう。

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

ルート パターンでパラメーター トランスフォーマーを使用するには、これを Program.csConstraintMap を使用して構成します。

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

ASP.NET Core フレームワークでは、エンドポイントを解決する URI の変換にパラメーター トランスフォーマーを使用します。 たとえば、パラメーター トランスフォーマーでは、areacontrolleractionpage を照合するために使用されるルート値が変換されます。

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

上記のルート テンプレートでは、アクション SubscriptionManagementController.GetAll は URI /subscription-management/get-all と照合されます。 パラメーター トランスフォーマーでは、リンクを生成するために使用されるルート値は変更されません。 たとえば、Url.Action("GetAll", "SubscriptionManagement") では /subscription-management/get-all が出力されます。

ASP.NET Core には、生成されたルートでパラメーター トランスフォーマーを使用する API 規則があります。

URL 生成参照

このセクションには、URL の生成で実装するアルゴリズムの参照情報が含まれています。 実際には、URL 生成の最も複雑な例で、コントローラーまたは Razor Pages が使用されます。 詳細については、コントローラーでのルーティングに関するページを参照してください。

URL の生成プロセスは、LinkGenerator.GetPathByAddress、または類似のメソッドへの呼び出しで開始されます。 このメソッドは、アドレス、一連のルート値、およびオプションで HttpContext からの現在の要求に関する情報と共に渡されます。

まずは、アドレスを使用して、アドレスの型に一致する IEndpointAddressScheme<TAddress> を使用して、一連の候補のエンドポイントが解決されます。

アドレス スキームによって一連の候補が検出されると、URL の生成操作が成功するまで、エンドポイントは反復的に並べ替えられ、処理されます。 URL が生成される際には、あいまいさの確認は行われず、最初に返される結果が最終的な結果になります。

ログを使用した URL 生成のトラブルシューティング

URL の生成のトラブルシューティングを行う場合、まずは Microsoft.AspNetCore.Routing のログ記録レベルを TRACE に設定します。 LinkGenerator では、問題のトラブルシューティングに役立つ、処理に関する多くの詳細がログに記録されます。

URL 生成の詳細については、「URL 生成参照」を参照してください。

アドレス

アドレスとは、リンク ジェネレーターへの呼び出しを一連の候補エンドポイントにバインドするために使用する、URL 生成の概念です。

アドレスとは、次の 2 つの実装を既定で備えた拡張可能な概念です。

  • アドレスとして "エンドポイント名" (string) を使用します。
    • MVC のルート名と同様の機能があります。
    • IEndpointNameMetadata メタデータ型を使用します。
    • 指定された文字列を、登録されているすべてのエンドポイントのメタデータに対して解決します。
    • 複数のエンドポイントが同じ名前を使用している場合は、起動時に例外をスローします。
    • コントローラーと Razor Pages 以外で汎用的に使用する場合にお勧めします。
  • ルート値 (RouteValuesAddress) をアドレスとして使用すると、次のようになります。
    • コントローラーおよび Razor Pages での従来の URL 生成と同様の機能があります。
    • 拡張およびデバッグする場合に非常に複雑です。
    • IUrlHelper、タグ ヘルパー、HTML ヘルパー、アクションの結果などで使用される実装を提供します。

アドレス スキームの役割は、任意の条件によって、アドレスと一致するエンドポイント間の関連付けを作成することです。

  • エンドポイント名スキームでは、基本的な辞書検索が実行されます。
  • ルート値のスキームには、セット アルゴリズムの複雑な最良のサブセットがあります。

アンビエント値と明示的な値

ルーティングは、現在の要求から現在の要求 HttpContext.Request.RouteValues のルート値にアクセスします。 現在の要求に関連付けられている値は、アンビエント値と呼ばれます。 このドキュメントでは、わかりやすくするために、メソッドに渡されるルート値を明示的な値と呼びます。

次の例では、アンビエント値と明示的な値を示しています。 ここでは、現在の要求のアンビエント値と明示的な値を提供します。

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

上記のコードでは次の操作が行われます。

  • /Widget/Index/17 を返します。
  • DI を介して LinkGenerator を取得します。

次のコードでは、明示的な値のみ提供されており、アンビエント値は提供されていません。

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

前述のメソッドでは /Home/Subscribe/17 が返されます。

WidgetController の次のコードでは、/Widget/Subscribe/17 が返されます。

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

次のコードは、現在の要求のアンビエント値と明示的な値からコントローラーを提供します。

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

上のコードでは以下の操作が行われます。

  • /Gadget/Edit/17 が返されます。
  • UrlIUrlHelper を取得します。
  • Action が、アクション メソッドの絶対パスを使用して URL を生成します。 URL には、指定した action 名と route 値が含まれます。

次のコードでは、現在の要求のアンビエント値と明示的な値を提供します。

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

前述のコードでは、Edit Razor ページに次のページ ディレクティブが含まれている場合に、url/Edit/17 に設定します。

@page "{id:int}"

[編集] ページに "{id:int}" ルート テンプレートが含まれない場合は、url/Edit?id=17 になります。

MVC の IUrlHelper の動作により、ここで説明するルールに加えて、複雑なレイヤーが追加されます。

  • IUrlHelper は、常に現在の要求からルート値をアンビエント値として提供します。
  • IUrlHelper.Action は、開発者がオーバーライドしない場合を除き、常に現在の actioncontroller ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の page ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の handler ルート値を明示的な値として null にオーバーライドします。

ユーザーは、MVC が独自のルールに従っていないように見えるため、アンビエント値の動作の詳細にしばしば驚きます。 これまでの経緯および互換性の理由により、actioncontrollerpagehandler などの特定のルート値には、独自の特殊な動作があります。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage の同等の機能では、互換性のために IUrlHelper のこの異常と同じ動作が行われます。

URL の生成処理

一連の候補エンドポイントが見つかると、URL の生成アルゴリズムでは次が実行されます。

  • エンドポイントが反復処理されます。
  • 最初に成功した結果が返されます。

このプロセスの最初の手順はルート値の無効化と呼ばれます。 ルート値の無効化は、アンビエント値からどのルート値を使用する必要があり、無視する必要があるかをルーティングが決定するプロセスです。 各アンビエント値が検討され、明示的な値と組み合わされるか、または無視されます。

アンビエント値の役割について一番わかりやすい考え方は、一部の一般的なケースでアプリケーション開発者の入力作業が省かれるということです。 従来、アンビエント値が役に立つシナリオは MVC に関連しています。

  • 同じコントローラー内の別のアクションにリンクする場合、コントローラー名を指定する必要はありません。
  • 同じ領域内の別のコントローラーにリンクする場合、領域名を指定する必要はありません。
  • 同じアクション メソッドにリンクする場合は、ルート値を指定する必要はありません。
  • アプリの別の部分にリンクする場合は、アプリのその部分には意味のないルート値は引き継ぎません。

null を返す LinkGenerator または IUrlHelper の呼び出しは、通常、ルート値の無効化について理解していないことが原因で発生します。 ルート値の無効化のトラブルシューティングを行うには、さらにルート値を明示的に指定して、これにより問題が解決されるかどうかを確認します。

ルート値の無効化は、アプリの URL スキームが階層的であり、階層が左から右に形成されていることを前提として機能します。 基本的なコントローラー ルート テンプレート {controller}/{action}/{id?} について考えてみましょう。これが実際にどのように動作するかを直感的に理解できます。 値に対する変更により、右側に表示されるすべてのルート値が無効化されます。 これには、階層に関する前提が反映されています。 アプリに id のアンビエント値があり、操作によって controller に対して異なる値が指定された場合、

  • {controller}{id?} の左側にあるため、id は再利用されません。

この原則を示すいくつかの例を次に示します。

  • 明示的な値に id の値が含まれている場合、id のアンビエント値は無視されます。 controlleraction のアンビエント値を使用できます。
  • 明示的な値に action の値が含まれている場合、action のアンビエント値はすべて無視されます。 controller のアンビエント値を使用できます。 action の明示的な値が action のアンビエント値と異なる場合、id 値は使用されません。 action の明示的な値が action のアンビエント値と同じ場合、id 値を使用できます。
  • 明示的な値に controller の値が含まれている場合、controller のアンビエント値はすべて無視されます。 controller の明示的な値が controller のアンビエント値と異なる場合、actionid の値は使用されません。 controller の明示的な値が controller のアンビエント値と同じ場合、actionid の値を使用できます。

このプロセスは、属性ルートと専用規則ルートが存在することでさらに複雑になります。 {controller}/{action}/{id?} などのコントローラーの規則ルートでは、ルート パラメーターを使用して階層が指定されます。 コントローラーと Razor Pages に対する専用規則ルート属性ルートの場合、

  • ルート値の階層があります。
  • テンプレートには表示されません。

このような場合は、URL の生成によって必要な値の概念が定義されます。 コントローラーおよび Razor Pages によって作成されたエンドポイントには、ルート値の無効化を機能させるために必要な値が指定されています。

ルート値の無効化アルゴリズムの詳細は次のとおりです。

  • 必要な値の名前がルート パラメーターと組み合わされ、左から右に処理されます。
  • 各パラメーターについて、アンビエント値と明示的な値が比較されます。
    • アンビエント値と明示的な値が同じ場合、プロセスは続行されます。
    • アンビエント値が存在し、明示的な値が存在しない場合は、URL を生成するときにアンビエント値が使用されます。
    • アンビエント値が存在せず、明示的な値が存在する場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。
    • アンビエント値と明示的な値が存在し、2 つの値が異なる場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。

この時点で、URL の生成操作はルート制約を評価する準備ができています。 許容可能な値のセットがパラメーターの既定値と組み合わされ、制約に提供されます。 すべての制約について合格した場合、操作が続行されます。

次に、許容可能な値を使用してルート テンプレートを展開できます。 ルート テンプレートは次のように処理されます。

  • 左から右。
  • 各パラメーターに、許容可能な値が代入されます。
  • 次の特殊なケースがあります。
    • 許容可能な値がなく、パラメーターに既定値がある場合は、既定値が使用されます。
    • 許容可能な値がなく、パラメーターが省略可能な場合は、処理が続行されます。
    • 存在しない省略可能なパラメーターの右側にあるルート パラメーターのいずれかに値がある場合、操作は失敗します。
    • 連続する既定値パラメーターと省略可能なパラメーターは、可能な場合、折りたたまれています。

ルートのセグメントと一致しない明示的に指定された値は、クエリ文字列に追加されます。 次の表は、ルート テンプレート {controller}/{action}/{id?} の使用時の結果をまとめたものです。

アンビエント値 明示的な値 結果
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

省略可能なルート パラメーターの順序

省略可能なルート パラメーターは、すべての必須のルート パラメーターの後に指定する必要があります。 次のコードで、idname のパラメーターは color パラメーターの後に指定する必要があります。

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}

ルート値の無効化に関する問題

次のコードは、ルーティングでサポートされていない URL 生成スキームの例を示しています。

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

上記のコードでは、culture ルート パラメーターがローカライズに使用されています。 culture パラメーターを常にアンビエント値として許容されるようにすることが望まれます。 しかし、必要な値の動作方法が理由で、culture パラメーターはアンビエント値として許容されません。

  • "default" ルート テンプレートでは、culture ルート パラメーターは controller の左側にあるため、controller を変更しても culture は無効になりません。
  • "blog" ルート テンプレートでは、culture ルート パラメーターは controller の右側にあると見なされ、必要な値に表示されます。

LinkParser で URL パスを解析する

LinkParser クラスは、URL パスをルート値のセットに解析するためのサポートを追加します。 ParsePathByEndpointName メソッドは、エンドポイント名と URL パスを受け取り、URL パスから抽出された一連のルート値を返します。

次のコントローラーの例では、GetProduct アクションは api/Products/{id} のルート テンプレートを使用し、GetProduct という Name を持ちます。

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

同じコントローラー クラス内で、AddRelatedProduct アクションには URL パス pathToRelatedProduct が必要です。これは、クエリ文字列パラメーターとして指定できます。

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

前の例では、AddRelatedProduct アクションによって URL パスから id ルート値が抽出されます。 たとえば、/api/Products/1 の URL パスが指定されている場合、relatedProductId 値は 1 に設定されます。 この方法により、API のクライアントは、リソースを参照するときに URL パスを使用できます。そのような URL の構造に関する知識は必要ありません。

エンドポイント メタデータを構成する

次のリンクでは、エンドポイント メタデータを構成する方法に関する情報を提供しています。

RequireHost とルートが一致するホスト

RequireHost では、指定したホストが必要であるという制約がルートに適用されます。 RequireHost または [Host] パラメーターには、以下を指定できます。

  • ホスト: www.domain.com。任意のポートの www.domain.com と一致します。
  • ホストとワイルドカード: *.domain.com。任意のポートの www.domain.comsubdomain.domain.com、または www.subdomain.domain.com と一致します。
  • ポート: *:5000。任意のホストのポート 5000 と一致します。
  • ホストとポート: www.domain.com:5000 または *.domain.com:5000。ホストとポートと一致します。

RequireHost または [Host] を使用して、複数のパラメーターを指定できます。 制約は、いずれかのパラメーターに対して有効なホストと一致します。 たとえば、[Host("domain.com", "*.domain.com")]domain.comwww.domain.com、および subdomain.domain.com と一致します。

次のコードでは、RequireHost を使用して、指定したホストをルートに対して要求します。

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

次のコードでは、コントローラー上の [Host] 属性を使用して、指定したホストのいずれかを要求します。

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

[Host] 属性がコントローラーとアクション メソッドの両方に適用される場合は、次のようになります。

  • アクションの属性が使用されます。
  • コントローラーの属性は無視されます。

ルート グループ

MapGroup 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、エンドポイント メタデータを追加する RequireAuthorizationWithMetadata のようなメソッドを 1 回呼び出すだけで、エンドポイントのグループ全体をカスタマイズできます。

たとえば、次のコードにより、2 つの似たエンドポイント グループが作成されます。

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

このシナリオでは、201 Created 結果の Location ヘッダーに相対アドレスを使用できます。

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

エンドポイントの最初のグループは、/public/todos のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 2 番目のグループは、/private/todos のプレフィックスが付いた要求にのみ一致し、認証が必要です。

QueryPrivateTodosエンドポイント フィルター ファクトリは、プライベート todo データにアクセスして格納できるようにルート ハンドラーの TodoDb パラメーターを変更するローカル関数です。

ルート グループでは、ルート パラメーターと制約を含む入れ子になったグループと複雑なプレフィックス パターンもサポートされます。 次の例で、user グループにマップされたルート ハンドラーは、外部グループ プレフィックスで定義されている {org} および {group} ルート パラメーターをキャプチャできます。

プレフィックスは空にすることもできます。 これは、ルート パターンを変更せずにエンドポイントのグループにエンドポイント メタデータまたはフィルターを追加する場合に役立ちます。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

フィルターまたはメタデータをグループに追加すると、内部グループまたは特定のエンドポイントに追加された可能性のある追加のフィルターまたはメタデータを追加する前に各エンドポイントに個別に追加する場合と同じように動作します。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

上記の例では、外部フィルターは、2 番目に追加された場合でも、内部フィルターの前に受信要求をログに記録します。 フィルターは異なるグループに適用されているため、互いが相対的に追加された順序は関係ありません。 同じグループまたは特定のエンドポイントに適用されている場合、追加される順序フィルターは重要です。

/outer/inner/ に対する要求によって、次がログに記録されます。

/outer group filter
/inner group filter
MapGet filter

ルーティングに関するパフォーマンス ガイダンス

アプリにパフォーマンス上の問題がある場合、多くの場合ルーティングが問題として疑われます。 ルーティングが疑われる理由は、コントローラーや Razor Pages などのフレームワークにより、フレームワーク内で費やされた時間がログ メッセージで報告されるためです。 コントローラーによって報告された時間と要求の合計時間の間に大きな違いがある場合、次のようになります。

  • 開発者は、問題の発生源としてアプリ コードを排除します。
  • ルーティングが原因であると考えるのが一般的です。

ルーティングは、数千のエンドポイントを使用してパフォーマンス テストされています。 一般的なアプリでは、大きすぎるだけでパフォーマンスの問題が発生する可能性はほとんどありません。 ルーティングのパフォーマンス低下の最も一般的な根本原因は、通常、正しく動作していないカスタム ミドルウェアです。

次のコード サンプルは、遅延の原因を絞り込むための基本的な手法を示したものです。

var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

ルーティングの時間は次のように計ります。

  • 各ミドルウェアを、上記のコードに示されている時間を計るミドルウェアのコピーでインターリーブします。
  • 計られた時間データをコードと関連付けるための一意の識別子を追加します。

これは、遅延が 10ms を超えるなど顕著な場合に絞り込むための基本的な方法です。 Time 1 から Time 2 を引くことで、UseRouting ミドルウェア内で費やされた時間を報告します。

次のコードでは、前の時間を計るコードに対して、よりコンパクトなアプローチを使用します。

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");

潜在的にコストが高いルーティング機能

次の一覧に、基本的なルート テンプレートと比べて比較的コストが高いルーティング機能についての洞察を示します。

  • 正規表現: 複雑な正規表現を作成すること、つまり少量の入力で長い実行時間を実現することができます。
  • 複雑なセグメント ({x}-{y}-{z}):
    • 通常の URL パス セグメントを解析するよりもかなりコストがかかります。
    • より多くの部分文字列が割り当てられることになります。
  • 同期データ アクセス: 多くの複雑なアプリでは、ルーティングの一部としてデータベースにアクセスします。 非同期的な MatcherPolicyEndpointSelectorContext などの拡張ポイントを使用します。

大規模なルート テーブルに関するガイダンス

既定では ASP.NET Core によって、メモリを CPU 時間とトレードするルーティング アルゴリズムが使用されます。 その効果として、ルート照合の時間が、照合するパスの長さにのみ左右され、ルートの数には左右されなくなります。 ただし、アプリに多数 (何千個) のルートがあり、大量の変数のプレフィックスがルートにあるようなケースでは、この方式が問題となる可能性があります。 たとえば、{parameter}/some/literal など、ルートの前方のセグメントにパラメーターが存在する場合があります。

以下のケースを除き、この方式が問題となる状況にアプリが陥る可能性は極めて低いです。

  • このパターンを使用するアプリに多数のルートがある。
  • アプリに多数のルートがある。

アプリで大規模なルート テーブルの問題が発生しているかどうかを判断する方法

  • 次の 2 つの症状がないかどうかを確認します。
    • 最初の要求でアプリの起動が遅い。
      • これは必須条件ですが、これだけでは不十分です。 ルート以外のさまざまな問題でも、アプリの起動が遅くなることがあります。 以下の条件を確認して、当該の状況がアプリで発生しているかどうかを正確に判断します。
    • アプリが起動時に大量のメモリを消費し、メモリ ダンプに多数の Microsoft.AspNetCore.Routing.Matching.DfaNode インスタンスが示されている。

この問題に対処する方法

ルートに適用可能ないくつかの手法や最適化を使用して、このシナリオを大幅に改善できます。

  • 可能な場合に、{parameter:int}{parameter:guid}{parameter:regex(\\d+)} などのルート制約をパラメーターに適用します。
    • これにより、ルーティング アルゴリズムで、照合に使用される構造を内部的に最適化し、使用メモリを大幅に削減できるようになります。
    • ほとんどの場合、許容できる動作に戻すのにこれで十分です。
  • テンプレート内の後方のセグメントにパラメーターを移動するように、ルートを変更します。
    • これにより、パスの指定されたエンドポイントに一致する可能性がある "パス" の数が減少します。
  • 動的ルートを使用し、コントローラー/ページへのマッピングを動的に実行します。
    • これは、MapDynamicControllerRoute および MapDynamicPageRoute を使用して実施できます。

ライブラリ作成者向けのガイダンス

このセクションでは、ルーティングを基盤とするライブラリ作成者向けのガイダンスを示します。 これらの詳細情報は、アプリの開発者が、ルーティングを拡張するライブラリとフレームワークを使用して優れたエクスペリエンスを実現できるようにすることを目的としています。

エンドポイントを定義する

URL 照合にルーティングを使用するフレームワークを作成するには、まず UseEndpoints の上に構築されるユーザー エクスペリエンスを定義します。

IEndpointRouteBuilder の上に構築します。 これにより、ユーザーは、他の ASP.NET Core 機能と混同せずにフレームワークを構成できます。 すべての ASP.NET Core テンプレートには、ルーティングが含まれます。 ルーティングが存在し、ユーザーになじみのあるものとします。

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

IEndpointConventionBuilder を実装する MapMyFramework(...) の呼び出しから、シールドの具象型を返します。 ほとんどのフレームワーク Map... メソッドは、このパターンに従います。 IEndpointConventionBuilder インターフェイスは、

  • メタデータを構成できます。
  • さまざまな拡張メソッドの対象とされています。

独自の型を宣言すると、独自のフレームワーク固有の機能をビルダーに追加できます。 フレームワークで宣言されたビルダーをラップし、呼び出しを転送するのは問題ありません。

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

独自の EndpointDataSource を作成することを検討しますEndpointDataSource は、エンドポイントのコレクションを宣言および更新するための低レベルのプリミティブです。 EndpointDataSource は、コントローラーと Razor Pages によって使用される強力な API です。

ルーティング テストには、更新されていないデータ ソースの基本的な例があります。

GetGroupedEndpoints を実装することを検討してください。 これにより、グループ化されたエンドポイントで実行されるグループ規則と最終的なメタデータを完全に制御できます。 たとえば、これにより、カスタム EndpointDataSource 実装でグループに追加されたエンドポイント フィルターを実行できます。

既定では、EndpointDataSource の登録を試行しないでくださいUseEndpoints にフレームワークを登録するようユーザーに要求してください。 ルーティングの原理では、既定では何も含まれておらず、UseEndpoints がエンドポイントを登録する場所です。

ルーティング統合ミドルウェアを作成する

メタデータ型をインターフェイスとして定義することを検討します

クラスおよびメソッドの属性としてメタデータ型を使用できるようにします

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

コントローラーや Razor Pages などのフレームワークでは、型およびメソッドへのメタデータ属性の適用がサポートされています。 メタデータ型を宣言する場合:

  • 属性としてアクセスできるようにします。
  • ほとんどのユーザーが属性の適用に精通しています。

メタデータ型をインターフェイスとして宣言すると、柔軟性の高いレイヤーがさらに追加されます。

  • インターフェイスが構成可能です。
  • 開発者は、複数のポリシーを結合する独自の型を宣言できます。

次の例に示すように、メタデータをオーバーライドできるようにします

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

これらのガイドラインに従う最善の方法は、マーカーのメタデータを定義しないようにすることです。

  • メタデータ型が存在するかどうかを確認するだけで終わらせません。
  • メタデータのプロパティを定義し、プロパティを確認します。

メタデータ コレクションが順序付けされ、優先順位によるオーバーライドがサポートされます。 コントローラーの場合、アクション メソッドのメタデータが最も限定的です。

ルーティングを使用する場合もしない場合もミドルウェアが役立つようにします

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

このガイドラインの例として、UseAuthorization ミドルウェアを考えてみましょう。 この承認ミドルウェアを使用すると、フォールバック ポリシーを渡すことができます。 フォールバック ポリシーは、指定されている場合、次の両方に適用されます。

  • 指定されたポリシーのないエンドポイント。
  • エンドポイントに一致しない要求。

これにより、承認ミドルウェアはルーティングのコンテキストの外部で役に立ちます。 承認ミドルウェアは、従来のミドルウェア プログラミングに使用できます。

デバッグ診断

詳細なルーティング診断出力を行うには、Logging:LogLevel:MicrosoftDebug に設定してください。 開発環境では、appsettings.Development.json でログ レベルを次のように設定します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

その他のリソース

ルーティングの役割は、受信した HTTP 要求を照合し、それらの要求をアプリの実行可能なエンドポイントにディスパッチすることです。 エンドポイントは、アプリの実行可能な要求処理コードの単位です。 エンドポイントはアプリで定義され、アプリの起動時に構成されます。 エンドポイントの照合プロセスでは、要求の URL から値を抽出し、それらの値を要求の処理に提供できます。 アプリからのルート情報を使用して、ルーティングでエンドポイントにマップする URL を生成することもできます。

アプリでは、次のものを使用してルーティングを構成できます。

この記事では、ASP.NET Core のルーティングについて詳しく説明します。 ルーティングの構成については、以下を参照してください。

ルーティングの基本

次のコードでは、ルーティングの基本的な例を示します。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前述の例には、MapGet メソッドを使用するエンドポイントが 1 つ含まれます。

  • HTTP GET 要求がルート URL / に送信された場合:
    • 要求デリゲートが実行されます。
    • Hello World! が HTTP 応答に書き込まれます。
  • 要求メソッドが GET ではない場合、またはルート URL が / ではない場合は、一致するルートはなく、HTTP 404 が返されます。

ルーティングでは、UseRoutingUseEndpoints によって登録されたミドルウェアのペアを使用します。

  • UseRouting では、ルートの照合がミドルウェア パイプラインに追加されます。 このミドルウェアによって、アプリで定義されているエンドポイントのセットが調べられ、要求に基づいて最適な一致が選択されます。
  • UseEndpoints では、エンドポイントの実行がミドルウェア パイプラインに追加されます。 選択されたエンドポイントに関連付けられているデリゲートが実行されます。

通常は、アプリで UseRouting または UseEndpoints を呼び出す必要はありません。 WebApplicationBuilder は、Program.cs に追加されたミドルウェアを UseRoutingUseEndpoints でラップするミドルウェア パイプラインを構成します。 ただし、アプリでは、これらのメソッドを明示的に呼び出すことによって、UseRoutingUseEndpoints の実行順序を変更できます。 たとえば、次のコードでは、UseRouting を明示的に呼び出しています。

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

上のコードでは以下の操作が行われます。

  • app.Use の呼び出しにより、パイプラインの開始時に実行されるカスタム ミドルウェアが登録されます。
  • UseRouting の呼び出しにより、ルートの照合ミドルウェアがカスタム ミドルウェアの "後に" 実行されるように構成されます。
  • MapGet に登録されたエンドポイントが、パイプラインの最後で実行されます。

前述の例に UseRouting の呼び出しが含まれていなかった場合、カスタム ミドルウェアは、ルートの照合ミドルウェアの "後に" 実行されます。

エンドポイント

エンドポイントを定義するには、MapGet メソッドが使用されます。 エンドポイントとは、次のようなものです。

  • URL と HTTP メソッドを一致させることによって選択できます。
  • デリゲートを実行することによって実行できます。

アプリによって一致させて実行できるエンドポイントは、UseEndpoints で構成します。 たとえば、MapGetMapPost、および類似のメソッドでは、要求のデリゲートがルーティング システムに接続されます。 他のメソッドを使用して、ASP.NET Core フレームワークの機能をルーティング システムに接続できます。

次の例では、より高度なルート テンプレートによるルーティングを示します。

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

文字列 /hello/{name:alpha} は、ルート テンプレートです。 ルート テンプレートは、エンドポイントの一致方法を構成するために使用されます。 この場合、テンプレートは次のものと一致します。

  • /hello/Docs のような URL
  • /hello/ で始まり、その後に一連の英字が続く任意の URL パス。 :alpha では、英字のみと一致するルート制約が適用されます。 ルート制約については、この記事で後ほど説明します。

URL パスの 2 番目のセグメント {name:alpha} は次のようになります。

  • name パラメーターにバインドされます。
  • キャプチャされて HttpRequest.RouteValues に格納されます。

次の例では、正常性チェックと承認を使用するルーティングを示します。

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

上の例では、次の方法が示されています。

  • ルーティングで承認ミドルウェアを使用できます。
  • エンドポイントを使用して、承認動作を構成できます。

MapHealthChecks の呼び出しにより、正常性チェック エンドポイントが追加されます。 この呼び出しに RequireAuthorization をチェーンすると、エンドポイントに承認ポリシーがアタッチされます。

UseAuthenticationUseAuthorization を呼び出すと、認証ミドルウェアと承認ミドルウェアが追加されます。 これらのミドルウェアは、次のことができるように UseRoutingUseEndpoints の間に配置されます。

  • UseRouting によって選択されたエンドポイントを確認します。
  • UseEndpoints によってエンドポイントにディスパッチされる前に、承認ポリシーを適用します。

エンドポイントのメタデータ

前の例には 2 つのエンドポイントがありますが、承認ポリシーがアタッチされているのは正常性チェック エンドポイントだけです。 要求が正常性チェック エンドポイント /healthz と一致した場合、承認チェックが実行されます。 これは、エンドポイントに追加のデータをアタッチできることを示しています。 この追加データは、エンドポイントのメタデータと呼ばれます。

  • メタデータは、ルーティング対応ミドルウェアによって処理できます。
  • メタデータには、任意の .NET 型を使用できます。

ルーティングの概念

ルーティング システムは、ミドルウェア パイプラインを基にして、強力なエンドポイント概念を追加することにより、構築されています。 エンドポイントは、ルーティング、承認、および任意の数の ASP.NET Core システムに関して相互に独立している、アプリの機能の単位を表します。

ASP.NET Core エンドポイントの定義

ASP.NET Core エンドポイントとは次のようなものです。

  • 実行可能: RequestDelegate を持っています。
  • 拡張可能: Metadata コレクションを持っています。
  • Selectable: 必要に応じて、ルーティング情報を持ちます。
  • 列挙可能: エンドポイントのコレクションの一覧は、DI から EndpointDataSource を取得することによって得られます。

次のコードでは、エンドポイントを取得し、現在の要求と一致するものを検査する方法を示します。

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

エンドポイントが選択されている場合は、HttpContext から取得できます。 そのプロパティを検査できます。 エンドポイント オブジェクトは不変であり、作成後に変更することはできません。 最も一般的なエンドポイントの型は RouteEndpoint です。 RouteEndpoint には、ルーティング システムによって選択されるための情報が含まれています。

前述のコードでは、app.Use によってインライン ミドルウェアが構成されます。

次のコードでは、パイプラインで app.Use が呼び出される場所によっては、エンドポイントが存在しない場合があることを示します。

// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

前述のサンプルでは、エンドポイントが選択されているかどうかを表示する Console.WriteLine ステートメントが追加されています。 わかりやすくするため、このサンプルでは、指定された / エンドポイントに表示名が割り当てられています。

前述のサンプルには、これらのミドルウェアがパイプライン内で実行されるタイミングを正確に制御するための UseRouting および UseEndpoints の呼び出しも含まれています。

このコードを / の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

このコード他の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

この出力は次のことを示しています。

  • UseRouting が呼び出される前は、エンドポイントは常に null になっています。
  • 一致が見つかった場合、エンドポイントは UseRoutingUseEndpoints の間で null 以外の値になります。
  • 一致が見つかると、UseEndpoints ミドルウェアはターミナルです。 ターミナル ミドルウェアについては、この記事で後ほど定義します。
  • UseEndpoints の後のミドルウェアは、一致が検出されなかった場合にのみ実行されます。

UseRouting ミドルウェアでは、SetEndpoint メソッドを使用して、エンドポイントが現在のコンテキストにアタッチされます。 UseRouting ミドルウェアをカスタム ロジックに置き換えることができ、その場合でもエンドポイントを使用する利点を得られます。 エンドポイントはミドルウェアのような低レベルのプリミティブであり、ルーティングの実装には結合されません。 ほとんどのアプリでは、UseRouting をカスタム ロジックに置き換える必要はありません。

UseEndpoints ミドルウェアは、UseRouting ミドルウェアと連携して使用するように設計されています。 エンドポイントを実行するためのコア ロジックは複雑ではありません。 GetEndpoint を使用してエンドポイントを取得し、その RequestDelegate プロパティを呼び出します。

次のコードでは、ミドルウェアがルーティングに与える影響またはルーティングに対応する方法を示します。

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

前の例では、2 つの重要な概念が示されています。

  • ミドルウェアは、UseRouting の前に実行して、ルーティングの動作に使用されるデータを変更できます。
  • ミドルウェアは、UseRoutingUseEndpoints の間で実行して、エンドポイントが実行される前にルーティングの結果を処理できます。
    • UseRoutingUseEndpoints の間で実行されるミドルウェア:
      • 通常、メタデータを検査してエンドポイントを認識します。
      • 多くの場合、UseAuthorizationUseCors によって実行されるセキュリティに関する決定を行います。
    • ミドルウェアとメタデータを組み合わせることで、エンドポイントごとにポリシーを構成できます。

上のコードでは、エンドポイントごとのポリシーをサポートするカスタム ミドルウェアの例が示されています。 ミドルウェアによって、機密データへのアクセスの "監査ログ" がコンソールに書き込まれます。 RequiresAuditAttribute メタデータを使用して、エンドポイントを "監査する" ようにミドルウェアを構成できます。 このサンプルでは、機密としてマークされているエンドポイントのみが監査される "オプトイン" パターンを示します。 このロジックの逆を定義して、たとえば安全としてマークされていないすべてのものを監査することができます。 エンドポイント メタデータ システムは柔軟です。 ユース ケースに適したどのような方法でも、このロジックを設計できます。

前のサンプル コードは、エンドポイントの基本的な概念を示すことが意図されています。 サンプルは運用環境での使用は意図されていません。 より完全なバージョンの "監査ログ" ミドルウェアでは、次のことが行われます。

  • ファイルまたはデータベースにログを記録します。
  • ユーザー、IP アドレス、機密性の高いエンドポイントの名前などの詳細情報が追加されます。

コントローラーや SignalR などのクラスベースのフレームワークで簡単に使用できるように、監査ポリシー メタデータ RequiresAuditAttributeAttribute と定義されています。 "コードへのルーティング" を使用すると、次のようになります。

  • メタデータがビルダー API にアタッチされます。
  • エンドポイントを作成するとき、クラス ベースのフレームワークに、対応するメソッドとクラスのすべての属性が組み込まれます。

メタデータの型に対するベスト プラクティスは、インターフェイスまたは属性として定義することです。 インターフェイスと属性では、コードを再利用できます。 メタデータ システムは柔軟であり、どのような制限もありません。

ターミナル ミドルウェアとルーティングを比較する

次の例は、ターミナル ミドルウェアとルーティングの両方を示しています。

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Approach 1: で示されているミドルウェアのスタイルは、ターミナル ミドルウェアです。 ターミナル ミドルウェアと呼ばれるのは、照合操作を実行するためです。

  • 前のサンプルの照合操作は、ミドルウェアの場合は Path == "/"、ルーティングの場合は Path == "/Routing" です。
  • 照合が成功すると、next ミドルウェアを呼び出すのではなく、一部の機能を実行して戻ります。

ターミナル ミドルウェアと呼ばれるのは、検索を終了し、いくつかの機能を実行してから制御を返すためです。

次の一覧では、ターミナル ミドルウェアとルーティングを比較しています。

  • どちらの方法でも、処理パイプラインを終了できます。
    • ミドルウェアでは、next を呼び出すのではなく、戻ることによってパイプラインが終了されます。
    • エンドポイントは常にターミナルです。
  • ターミナル ミドルウェアを使用すると、パイプライン内の任意の場所にミドルウェアを配置できます。
    • エンドポイントは、UseEndpoints の位置で実行されます。
  • ターミナル ミドルウェアでは、任意のコードを使用してミドルウェアが一致するかどうかを判定できます。
    • カスタム ルート一致コードは、冗長で、正しく記述するのが困難な場合があります。
    • ルーティングでは、一般的なアプリに対して簡単なソリューションが提供されます。 ほとんどのアプリでは、カスタム ルート一致コードは必要ありません。
  • UseAuthorizationUseCors などのミドルウェアを使用したエンドポイント インターフェイス。
    • UseAuthorization または UseCors でターミナル ミドルウェアを使用するには、承認システムとの手動インターフェイスが必要です。

エンドポイント では、次の両方が定義されます。

  • 要求を処理するためのデリゲート。
  • 任意のメタデータのコレクション。 メタデータは、各エンドポイントにアタッチされている構成とポリシーに基づいて横断的な関心事を実装するために使用されます。

ターミナル ミドルウェアは効果的なツールになる可能性がありますが、次のものが必要です。

  • 大量のコーディングとテスト。
  • 必要なレベルの柔軟性を実現するための、他のシステムとの手作業による統合。

ターミナル ミドルウェアを作成する前に、ルーティングとの統合を検討してください。

Map または MapWhen と統合されている既存のターミナル ミドルウェアは、通常、ルーティング対応のエンドポイントにすることができます。 MapHealthChecks では、ルーターウェアのパターンが示されています。

  • IEndpointRouteBuilder で拡張メソッドを作成します。
  • CreateApplicationBuilder を使用して、入れ子になったミドルウェア パイプラインを作成します。
  • ミドルウェアを新しいパイプラインにアタッチします。 例では、 UseHealthChecksが使用されます。
  • RequestDelegate にミドルウェア パイプラインを Build します。
  • Map を呼び出し、新しいミドルウェア パイプラインを提供します。
  • 拡張メソッドから Map によって提供されるビルダー オブジェクトを返します。

次のコードでは、MapHealthChecks の使用方法を示します。

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

前のサンプルでは、ビルダー オブジェクトを返すことが重要である理由が示されています。 ビルダー オブジェクトを返すことで、アプリ開発者はエンドポイントの承認などのポリシーを構成できます。 この例では、正常性チェック ミドルウェアと承認システムは直接統合されていません。

そのメタデータ システムは、ターミナル ミドルウェアを使用する機能拡張作成者によって、発生する問題に対応して作成されました。 各ミドルウェアで承認システムとの独自の統合を実装することには問題があります。

URL 一致

  • ルーティングによって受信要求がエンドポイントと照合されるプロセスです。
  • URL パスとヘッダーのデータに基づいています。
  • 要求内の任意のデータを考慮するように拡張できます。

実行されたルーティング ミドルウェアでは、Endpoint が設定され、現在の要求からの HttpContext要求機能に値がルーティングされます。

  • HttpContext.GetEndpoint を呼び出すと、エンドポイントが取得されます。
  • HttpRequest.RouteValues では、ルート値のコレクションが取得されます。

ルーティング ミドルウェアの後で実行されたミドルウェアでは、エンドポイントを調べて、アクションを実行することができます。 たとえば、承認ミドルウェアでは、エンドポイントのメタデータ コレクションに対し、承認ポリシーを問い合わせることができます。 要求処理パイプライン内のすべてのミドルウェアが実行された後、選択したエンドポイントのデリゲートが呼び出されます。

エンドポイント ルーティングのルーティング システムでは、配布に関するすべての決定が行われます。 ミドルウェアでは選択されたエンドポイントに基づいてポリシーが適用されるため、次のことが重要です。

  • ディスパッチまたはセキュリティ ポリシーの適用に影響を与える可能性のある決定は、ルーティング システム内で行われます。

警告

後方互換性のために、コントローラーまたは Razor Pages エンドポイント デリゲートが実行されると、それまでに実行された要求処理に基づいて、RouteContext.RouteData のプロパティが適切な値に設定されます。

RouteContext の種類は、今後のリリースでは古いものとしてマークされます。

  • RouteData.ValuesHttpRequest.RouteValues に移行します。
  • RouteData.DataTokens を移行して、エンドポイント メタデータから IDataTokensMetadata を取得します。

URL の照合は、構成可能な一連のフェーズで動作します。 各フェーズでの出力は一致のセットとなります。 一致のセットは、次のフェーズでさらに絞り込むことができます。 ルーティングの実装では、一致するエンドポイントの処理順序は保証されません。 一致の可能性のあるものは一度にすべて処理されます。 URL 照合フェーズは、次の順序で発生します。 ASP.NET Core:

  1. エンドポイントのセットおよびそれらのルート テンプレートに対して URL パスを処理し、すべての一致を収集します。
  2. 前のリストを取得し、ルート制約が適用されると失敗する一致を削除します。
  3. 前のリストを取得し、MatcherPolicy インスタンスのセットを失敗させる一致を削除します。
  4. EndpointSelector を使用して、前のリストから最終的な決定を行います。

エンドポイントのリストは、次の内容に従って優先度付けが行われます。

EndpointSelector に到達するまで、各フェーズで一致するすべてのエンドポイントが処理されます。 EndpointSelector は最後のフェーズです。 一致の中から最も優先度の高いエンドポイントが最適な一致として選択されます。 最適な一致と優先度が同じである一致が他にもある場合は、あいまい一致の例外がスローされます。

ルートの優先順位はより具体的なルート テンプレートに、より高い優先度が与えられることに基づいて算出されます。 たとえば、テンプレート /hello/{message} を検討してみます。

  • どちらも URL パス /hello と一致します。
  • /hello の方がより具体的であるため、優先度が高くなります。

一般に、ルートの優先順位は、実際に使用される URL スキームの種類として最適なものを選択するのに適しています。 Order は、あいまいさを避けるために必要な場合にのみ使用します。

拡張性の種類がルーティングによって指定されるため、あいまいなルートを事前にルーティング システムによって計算することはできません。 ルート テンプレート /{message:alpha}/{message:int} などの例を考えてみましょう。

  • alpha 制約を使用すると、アルファベット文字のみと一致します。
  • int 制約を使用すると、数値のみと一致します。
  • これらのテンプレートのルート優先順位は同じですが、この両方と一致する単一の URL はありません。
  • 起動時にルーティング システムからあいまいエラーが報告された場合、それによってこの有効なユース ケースはブロックされます。

警告

UseEndpoints 内での処理の順序は、ルーティングの動作には影響しませんが、例外が 1 つあります。 MapControllerRoute および MapAreaRoute では、それぞれが呼び出された順序に基づいて、それぞれのエンドポイントに順序値が自動的に割り当てられます。 これにより、ルーティング システムでより古いルーティング実装と同じ保証を提供しなくても、コントローラーの長時間の動作がシミュレートされます。

ASP.NET Core でのエンドポイントのルーティングは、

  • ルートの概念がありません。
  • 順序付けが保証されません。 すべてのエンドポイントが一度に処理されます。

ルート テンプレートの優先順位とエンドポイントの選択順序

ルート テンプレートの優先順位とは、どれほど具体的であるかに基づいて、各ルート テンプレートに値を割り当てるシステムです。 ルート テンプレートの優先順位:

  • 一般的なケースでは、エンドポイントの順序を調整する必要はなくなります。
  • 一般的に期待されるルーティング動作との一致が試みられます。

たとえば、テンプレート /Products/List/Products/{id} について考えてみます。 URL パス /Products/List に対しては、/Products/List の方が /Products/{id} よりも適していると想定するのが妥当です。 このように言えるのは、リテラル セグメント /List がパラメーター セグメント /{id} よりも優先順位が高いと見なされるためです。

優先順位のしくみの詳細は、ルート テンプレートの定義方法と関連付けられています。

  • より多くのセグメントを持つテンプレートは、より具体的なものと見なされます。
  • リテラル テキストを含むセグメントは、パラメーター セグメントよりも具体的であると見なされます。
  • 制約が含まれるパラメーター セグメントは、それが含まれないものよりも具体的であると見なされます。
  • 複雑なセグメントは、制約を含むパラメーター セグメントと同じくらい具体的であると見なされます。
  • キャッチオール パラメーターは、まったく具体的ではありません。 キャッチオール ルートに関する重要な情報については、「ルート テンプレート」セクションのキャッチオールに関する記述を参照してください。

URL 生成の概念

URL の生成:

  • ルーティングにおいて、一連のルート値に基づいて URL パスを作成するプロセスです。
  • エンドポイントとそれにアクセスする URL を論理的に分離できます。

エンドポイント ルーティングには、LinkGenerator API が含まれます。 LinkGeneratorDI から使用できるシングルトン サービスです。 LinkGenerator API は、実行中の要求のコンテキスト外で使用することができます。 Mvc.IUrlHelper と、タグ ヘルパー、HTML ヘルパー、アクション結果など、IUrlHelper に依存するシナリオでは LinkGenerator API を内部的に使用して、リンク生成機能が提供されます。

リンク ジェネレーターは、アドレスアドレス スキーム の概念に基づいています。 アドレス スキームは、リンク生成で考慮すべきエンドポイントを決定する方法です。 たとえば、コントローラーおよび Razor Pages からの、多くのユーザーに馴染みのあるルート名やルート値シナリオは、アドレス スキームとして実装されます。

リンク ジェネレーターでは、次の拡張メソッドを介して、コントローラーおよび Razor Pages にリンクできます。

これらのメソッドのオーバーロードでは、HttpContext を含む引数が受け入れられます。 これらのメソッドは Url.Action および Url.Page と機能的には同等ですが、柔軟性とオプションがさらに提供されます。

GetPath* メソッドは、絶対パスを含む URI を生成するという点で Url.Action および Url.Page に最も似ています。 GetUri* メソッドでは常に、スキームとホストを含む絶対 URI が生成されます。 HttpContext を受け入れるメソッドでは、実行中の要求のコンテキストで URI が生成されます。 実行中の要求からのアンビエント ルート値、URL ベース パス、スキーム、およびホストは、オーバーライドされない限り使用されます。

LinkGenerator はアドレスと共に呼び出されます。 URI の生成は、次の 2 つの手順で行われます。

  1. アドレスは、そのアドレスと一致するエンドポイントのリストにバインドされます。
  2. 各エンドポイントの RoutePattern は、指定された値と一致するルート パターンが見つかるまで評価されます。 結果の出力は、リンク ジェネレーターに指定された他の URI 部分と結合され、返されます。

LinkGenerator によって提供されるメソッドでは、すべての種類のアドレスの標準的なリンク生成機能がサポートされます。 リンク ジェネレーターを使用する最も便利な方法は、特定のアドレスの種類の操作を実行する拡張メソッドを使用することです。

拡張メソッド 説明
GetPathByAddress 指定された値に基づき、絶対パスを含む URI を生成します。
GetUriByAddress 指定された値に基づき、絶対 URI を生成します。

警告

LinkGenerator メソッド呼び出しによる次の影響に注意してください。

  • 受信要求の Host ヘッダーが確認されないアプリ構成では、GetUri* 拡張メソッドは注意して使用してください。 受信要求の Host ヘッダーが確認されていない場合、信頼されていない要求入力を、ビューまたはページの URI でクライアントに送り返すことができます。 すべての運用アプリで、Host ヘッダーを既知の有効な値と照らし合わせて確認するようにサーバーを構成することをお勧めします。

  • ミドルウェアで Map または MapWhen と組み合わせて、LinkGenerator を使用する場合は注意してください。 Map* では、実行中の要求の基本パスが変更され、リンク生成の出力に影響します。 すべての LinkGenerator API で基本パスを指定することができます。 リンク生成への Map* の影響を元に戻すための空の基本パスを指定してください。

ミドルウェアの例

次の例では、ミドルウェアで LinkGenerator API を使用して、商品をリストするアクション メソッドへのリンクを作成します。 リンク ジェネレーターは、クラスに挿入し、GenerateLink を呼び出すことで、アプリのどのクラスでも使用できます。

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

ルート テンプレート

{} 内のトークンでは、ルートが一致した場合にバインドされるルート パラメーターが定義されます。 1 つのルート セグメントに複数のルート パラメーターを定義できますが、各ルート パラメーターをリテラル値で区切る必要があります。 次に例を示します。

{controller=Home}{action=Index}

これは、{controller}{action} の間にリテラル値がないため、有効なルートではありません。 ルート パラメーターには名前を付ける必要があります。付加的な属性を指定することもあります。

ルート パラメーター以外のリテラル テキスト ({id} など) とパス区切り / は URL のテキストに一致する必要があります。 テキスト照合は復号された URL のパスを基盤とし、大文字と小文字が区別されます。 リテラル ルート パラメーターの区切り記号 ({ または }) を照合するには、文字を繰り返して区切り記号をエスケープします。 たとえば、{{ または }} です。

アスタリスク * または二重アスタリスク**:

  • ルート パラメーターのプレフィックスとして使用して、URI の残りの部分にバインドすることができます。
  • キャッチオール パラメーターと呼ばれています。 blog/{**slug} の例を次に示します。
    • blog/ で始まり、その後に任意の値が続く URI と一致します。
    • blog/ に続く値は、slug ルート値に割り当てられます。

警告

ルーティングでバグが原因で、キャッチオール パラメーターがルートと正しく一致しない可能性があります。 このバグの影響を受けるアプリには、次の特性があります。

  • キャッチオール ルート (たとえば、{**slug}")
  • キャッチオール ルートが、一致すべき要求と一致しません。
  • 他のルートを削除すると、キャッチオール ルートが機能し始めます。

このバグが発生するケースの例については、GitHub のバグ 18677 および 16579 を参照してください。

このバグのオプトイン修正は .NET Core 3.1.301 SDK 以降に含まれています。 次のコードにより、このバグを修正する内部スイッチが設定されます。

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

キャッチオール パラメーターは空の文字列に一致することもあります。

キャッチオール パラメーターでは、パス区切り / 文字を含め、URL の生成にルートが使用されるときに適切な文字がエスケープされます。 たとえば、ルート値が { path = "my/path" } のルート foo/{*path} では、foo/my%2Fpath が生成されます。 エスケープされたスラッシュに注意してください。 パス区切り文字をラウンドトリップさせるには、** ルート パラメーター プレフィックスを使用します。 { path = "my/path" } のルート foo/{**path} では、foo/my/path が生成されます。

任意のファイル拡張子が付いたファイル名のキャプチャを試行する URL パターンには、追加の考慮事項があります。 たとえば、テンプレート files/{filename}.{ext?} について考えてみます。 filenameext の両方の値が存在するときに、両方の値が入力されます。 URL に filename の値だけが存在する場合、末尾の . は任意であるため、このルートは一致となります。 次の URL はこのルートに一致します。

  • /files/myFile.txt
  • /files/myFile

ルート パラメーターには、既定値 が含まれることがあります。パラメーター名の後に既定値を指定し、等号 (=) で区切ることで指定されます。 たとえば、{controller=Home} では、controller の既定値として Home が定義されます。 パラメーターの URL に値がない場合、既定値が使用されます。 ルート パラメーターは、パラメーター名の終わりに疑問符 (?) を追加することでオプションとして扱われます。 たとえば、id? のようにします。 省略可能な値と既定のルート パラメーターの違いは次のとおりです。

  • 既定値を持つルート パラメーターでは常に値が生成されます。
  • 省略可能なパラメーターには、要求 URL によって値が指定された場合にのみ値が含められます。

ルート パラメーターには、URL からバインドされるルート値に一致しなければならないという制約が含まれることがあります。 : と制約名をルート パラメーター名の後に追加すると、ルート パラメーターのインライン制約が指定されます。 その制約で引数が要求される場合、制約名の後にかっこ (...) で囲まれます。 複数の "インライン制約" を指定するには、別の : と制約名を追加します。

制約名と引数が IInlineConstraintResolver サービスに渡され、URL 処理で使用する IRouteConstraint のインスタンスが作成されます。 たとえば、ルート テンプレート blog/{article:minlength(10)} によって、制約 minlength と引数 10 が指定されます。 ルート制約の詳細とこのフレームワークによって指定される制約のリストについては、「ルート制約」セクションを参照してください。

ルート パラメーターには、パラメーター トランスフォーマーを指定することもできます。 パラメーター トランスフォーマーを指定すると、リンクを生成し、アクションおよびページを URL と一致させるときにパラメーターの値が変換されます。 制約と同様に、パラメーター トランスフォーマーをルート パラメーターにインラインで追加することができます。その場合、ルート パラメーター名の後に : とトランスフォーマー名を追加します。 たとえば、ルート テンプレート blog/{article:slugify} では、slugify トランスフォーマーが指定されます。 パラメーター トランスフォーマーの詳細については、「パラメーター トランスフォーマー」セクションを参照してください。

次の表に、ルート テンプレートの例とその動作を示します。

ルート テンプレート 一致する URI の例 要求 URI
hello /hello 単一パス /hello にのみ一致します。
{Page=Home} / 一致し、PageHome に設定されます。
{Page=Home} /Contact 一致し、PageContact に設定されます。
{controller}/{action}/{id?} /Products/List Products コントローラーと List アクションにマッピングされます。
{controller}/{action}/{id?} /Products/Details/123 Products コントローラーと Details アクションにマッピングされ、id は 123 に設定されます。
{controller=Home}/{action=Index}/{id?} / Home コントローラーと Index メソッドにマッピングされます。 id は無視されます。
{controller=Home}/{action=Index}/{id?} /Products Products コントローラーと Index メソッドにマッピングされます。 id は無視されます。

一般的に、テンプレートの利用が最も簡単なルーティングの手法となります。 ルート テンプレート以外では、制約と既定値も指定できます。

複雑なセグメント

複雑なセグメントは、リテラル区切り文字を右から左に最短一致の方法で照合することによって処理されます。 たとえば、[Route("/a{b}c{d}")] は複雑なセグメントです。 複雑なセグメントは、それらを適切に使用する上で理解する必要がある特定の方法で機能します。 このセクションの例では、パラメーター値の中に区切り文字が含まれていない場合にのみ、複雑なセグメントが本当にうまく機能する理由を示します。 より複雑なケースでは、regex を使用し、値を手動で抽出する必要があります。

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

これは、ルーティングがテンプレート /a{b}c{d} と URL パス /abcd を使用して実行するステップの概要です。 | は、アルゴリズムの動作を視覚化するために使用されます。

  • 最初のリテラル (右から左へ) は c です。 そこで、/abcd は右から検索され、/ab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /ab|c|d は中断したところから検索されて、a が見つかり、/|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • 残りのテキストも残りのルート テンプレートも存在しないため、これは一致となります。

同じテンプレート /a{b}c{d} と、URL パス /aabcd を使用した場合の否定の例を次に示します。 | は、アルゴリズムの動作を視覚化するために使用されます。 このケースは一致ではありませんが、同じアルゴリズムで説明します。

  • 最初のリテラル (右から左へ) は c です。 そこで、/aabcd は右から検索され、/aab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /aab|c|d は中断したところから検索されて、a が見つかり、/a|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • この時点で、テキスト a が残っていますが、アルゴリズムは解析するためのルート テンプレートを使い果たしたので、これは一致とはなりません。

照合アルゴリズムは最短一致のため、次のようになります。

  • 各ステップで可能な限りの最短のテキストに一致します。
  • パラメーター値の内部に区切り記号の値が表示されている場合は一致しません。

正規表現を使用すると、一致の動作をより細かく制御できます。

最長一致 (遅延一致 とも呼ばれる) を使用すると、可能な限り長い文字列と一致します。 最短一致は、可能な限り最短の文字列と一致します。

特殊文字を含むルーティング

特殊文字を含むルーティングは、予期しない結果になる可能性があります。 たとえば、次のアクション メソッドを含むコントローラーについて考えます。

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

string id に次のエンコードされた値が含まれる場合、予期しない結果が発生する可能性があります。

ASCII Encoded
/ %2F
+

ルート パラメーターは常に URL でデコードされるとは限りません。 この問題は今後対処される可能性があります。 詳しくは、こちらの GitHub イシューに関するページをご覧ください。

ルート制約

ルート制約は、受信 URL と一致し、URL パスがルート値にトークン化されたときに実行されます。 ルート制約では、通常、ルート テンプレート経由で関連付けられるルート値を調べ、値が許容できるかどうかを true または false で決定します。 一部のルート制約では、ルート値以外のデータを使用し、要求をルーティングできるかどうかが考慮されます。 たとえば、HttpMethodRouteConstraint はその HTTP Verb に基づいて要求を承認または却下します。 制約は、要求のルーティングとリンクの生成で使用されます。

警告

入力の検証には制約を使用しないでください。 入力の検証に制約を使用した場合、入力が無効だと "404 見つかりません" が返されます。 無効な入力の場合は、"400 要求が無効です" と適切なエラー メッセージが生成されます。 ルート制約は、特定のルートに対する入力の妥当性を検証するためではなく、似たようなルートの違いを明らかにするために使用されます。

次の表では、ルート制約の例とそれに求められる動作をまとめています。

制約 一致の例 メモ
int {id:int} 123456789-123456789 あらゆる整数に一致する
bool {active:bool} trueFALSE true または false に一致する。 大文字と小文字は区別されない
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm インバリアント カルチャの有効な DateTime 値に一致します。 前の警告を参照してください。
decimal {price:decimal} 49.99-1,000.01 インバリアント カルチャの有効な decimal 値に一致します。 前の警告を参照してください。
double {weight:double} 1.234-1,001.01e8 インバリアント カルチャの有効な double 値に一致します。 前の警告を参照してください。
float {weight:float} 1.234-1,001.01e8 インバリアント カルチャの有効な float 値に一致します。 前の警告を参照してください。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 有効な Guid 値に一致する
long {ticks:long} 123456789-123456789 有効な long 値に一致する
minlength(value) {username:minlength(4)} Rick 4 文字以上の文字列であることが必要
maxlength(value) {filename:maxlength(8)} MyFile 8 文字以内の文字列であることが必要
length(length) {filename:length(12)} somefile.txt 厳密に 12 文字の文字列であることが必要
length(min,max) {filename:length(8,16)} somefile.txt 8 文字以上、16 文字以内の文字列であることが必要
min(value) {age:min(18)} 19 18 以上の整数値であることが必要
max(value) {age:max(120)} 91 120 以下の整数値であることが必要
range(min,max) {age:range(18,120)} 91 18 以上、120 以下の整数値であることが必要
alpha {name:alpha} Rick 文字列は 1 つまたは複数のアルファベット文字で構成されることが必要 (a-z、大文字と小文字は区別されません)。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 文字列は正規表現と一致する必要があります。 正規表現の定義に関するヒントを参照してください。
required {name:required} Rick URL 生成中、非パラメーターが提示されるように強制する

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

1 のパラメーターには、複数の制約をコロンで区切って適用できます。 たとえば、次の制約では、パラメーターが 1 以上の整数値に制限されます。

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

警告

URL の妥当性を検証し、CLR 型に変換するルート制約では、常にインバリアント カルチャが使用されます。 たとえば、int または DateTime の CLR 型に変換される場合などです。 これらの制約では、URL のローカライズが不可であることが前提です。 フレームワークから提供されるルート制約がルート値に格納されている値を変更することはありません。 URL から解析されたルート値はすべて文字列として格納されます。 たとえば、float 制約はルート値を浮動小数に変換しますが、変換された値は、浮動小数に変換できることを検証するためにだけ利用されます。

正規表現の制約

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

正規表現は、regex(...) ルート制約を使用して、インライン制約として指定できます。 MapControllerRoute ファミリのメソッドでも、制約のオブジェクト リテラルを取ります。 この形式が使用されている場合、文字列値は正規表現として解釈されます。

次のコードでは、インラインで regex 制約が使用されています。

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

次のコードでは、regex 制約の指定にオブジェクト リテラルが使用されています。

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

ASP.NET Core フレームワークでは、正規表現コンストラクターに RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant が追加されます。 これらのメンバーの詳細については、「RegexOptions」を参照してください。

正規表現では、ルーティングや C# 言語で使用されるものに似た区切り記号とトークンが使用されます。 正規表現トークンはエスケープする必要があります。 インライン制約で正規表現 ^\d{3}-\d{2}-\d{4}$ を使用するには、次のいずれかを使用します。

  • \ 文字列エスケープ文字をエスケープするには、文字列で指定した \ 文字を、C# ソース ファイル内の \\ 文字に置き換えます。
  • 逐語的文字列リテラル

ルーティング パラメーター区切り記号文字 ({}[]) をエスケープするには、表現の文字を二重にします (例: {{}}[[]])。 次の表に、正規表現とそれにエスケープを適用した後のものを示します。

正規表現 エスケープ適用後の正規表現
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

ルーティングで使用される正規表現は、多くの場合、^ 文字で始まり、これは文字列の開始位置と一致します。 この式は、多くの場合、$ 文字で終わり、文字列の末尾と一致します。 ^ 文字と $ 文字により、正規表現がルート パラメーター値全体に一致することが保証されます。 ^ 文字と $ 文字がなければ、意図に反し、正規表現は文字列内のあらゆる部分文字列に一致してしまいます。 下の表では、一致または不一致の理由を例を示し説明します。

正規表現 String 一致したもの コメント
[a-z]{2} hello はい サブ文字列の一致
[a-z]{2} 123abc456 はい サブ文字列の一致
[a-z]{2} mz はい 一致する表現
[a-z]{2} MZ はい 大文字と小文字の使い方が違う
^[a-z]{2}$ hello いいえ 上の ^$ を参照
^[a-z]{2}$ 123abc456 いいえ 上の ^$ を参照

正規表現構文の詳細については、.NET Framework 正規表現に関するページを参照してください。

既知の入力可能値の集まりにパラメーターを制限するには、正規表現を使用します。 たとえば、{action:regex(^(list|get|create)$)} の場合、action ルート値は listgetcreate とのみ照合されます。 制約ディクショナリに渡された場合、文字列 ^(list|get|create)$ で同じものになります。 既知の制約に一致しない、制約ディクショナリに渡された制約も、正規表現として扱われます。 既知の制約に一致しない、テンプレート内で渡される制約は、正規表現としては扱われません。

カスタム ルート制約

カスタム ルート制約は、IRouteConstraint インターフェイスを実装して作成できます。 IRouteConstraint インターフェイスには、Match が含まれています。これでは、制約が満たされている場合は true を返し、それ以外の場合は false を返します。

カスタム ルート制約は通常必要ありません。 カスタム ルート制約を実装する前に、モデル バインドなどの代替手段を検討してください。

ASP.NET Core の Constraints フォルダーには、制約を作成するための適切な例が用意されています。 たとえば、GuidRouteConstraint です。

カスタムの IRouteConstraint を使うには、サービス コンテナー内の ConstraintMap に、ルート制約の種類が登録されている必要があります。 ConstraintMap は、ルート制約キーを、その制約を検証する IRouteConstraint の実装にマッピングするディクショナリです。 アプリの ConstraintMap は、Program.cs で、AddRouting 呼び出しの一部として、または builder.Services.Configure<RouteOptions> を使って RouteOptions を直接構成することで、更新できます。 次に例を示します。

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

上記の制約は、次のコードに適用されます。

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

NoZeroesRouteConstraint を実装することにより、ルート パラメーターで 0 が使用されるのを回避できます。

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

上記のコードでは次の操作が行われます。

  • ルートの {id} セグメントの 0 を禁止します。
  • カスタム制約を実装する基本的な例を示しています。 実稼働しているアプリでは使用しないでください。

次のコードは、0 を含む id が処理されないようにする優れた方法です。

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

上記のコードには NoZeroesRouteConstraint アプローチに対し、次の利点があります。

  • カスタム制約が必要ありません。
  • ルート パラメーターに 0 が含まれている場合は、よりわかりやすいエラーが返されます。

パラメーター トランスフォーマー

パラメーター トランスフォーマー:

たとえば、Url.Action(new { article = "MyTestArticle" }) のルート パターン blog\{article:slugify} のカスタム slugify パラメーター トランスフォーマーでは、blog\my-test-article が生成されます。

IOutboundParameterTransformer の次の実装を見てみましょう。

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

ルート パターンでパラメーター トランスフォーマーを使用するには、これを Program.csConstraintMap を使用して構成します。

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

ASP.NET Core フレームワークでは、エンドポイントを解決する URI の変換にパラメーター トランスフォーマーを使用します。 たとえば、パラメーター トランスフォーマーでは、areacontrolleractionpage を照合するために使用されるルート値が変換されます。

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

上記のルート テンプレートでは、アクション SubscriptionManagementController.GetAll は URI /subscription-management/get-all と照合されます。 パラメーター トランスフォーマーでは、リンクを生成するために使用されるルート値は変更されません。 たとえば、Url.Action("GetAll", "SubscriptionManagement") では /subscription-management/get-all が出力されます。

ASP.NET Core には、生成されたルートでパラメーター トランスフォーマーを使用する API 規則があります。

URL 生成参照

このセクションには、URL の生成で実装するアルゴリズムの参照情報が含まれています。 実際には、URL 生成の最も複雑な例で、コントローラーまたは Razor Pages が使用されます。 詳細については、コントローラーでのルーティングに関するページを参照してください。

URL の生成プロセスは、LinkGenerator.GetPathByAddress、または類似のメソッドへの呼び出しで開始されます。 このメソッドは、アドレス、一連のルート値、およびオプションで HttpContext からの現在の要求に関する情報と共に渡されます。

まずは、アドレスを使用して、アドレスの型に一致する IEndpointAddressScheme<TAddress> を使用して、一連の候補のエンドポイントが解決されます。

アドレス スキームによって一連の候補が検出されると、URL の生成操作が成功するまで、エンドポイントは反復的に並べ替えられ、処理されます。 URL が生成される際には、あいまいさの確認は行われず、最初に返される結果が最終的な結果になります。

ログを使用した URL 生成のトラブルシューティング

URL の生成のトラブルシューティングを行う場合、まずは Microsoft.AspNetCore.Routing のログ記録レベルを TRACE に設定します。 LinkGenerator では、問題のトラブルシューティングに役立つ、処理に関する多くの詳細がログに記録されます。

URL 生成の詳細については、「URL 生成参照」を参照してください。

アドレス

アドレスとは、リンク ジェネレーターへの呼び出しを一連の候補エンドポイントにバインドするために使用する、URL 生成の概念です。

アドレスとは、次の 2 つの実装を既定で備えた拡張可能な概念です。

  • アドレスとして "エンドポイント名" (string) を使用します。
    • MVC のルート名と同様の機能があります。
    • IEndpointNameMetadata メタデータ型を使用します。
    • 指定された文字列を、登録されているすべてのエンドポイントのメタデータに対して解決します。
    • 複数のエンドポイントが同じ名前を使用している場合は、起動時に例外をスローします。
    • コントローラーと Razor Pages 以外で汎用的に使用する場合にお勧めします。
  • ルート値 (RouteValuesAddress) をアドレスとして使用すると、次のようになります。
    • コントローラーおよび Razor Pages での従来の URL 生成と同様の機能があります。
    • 拡張およびデバッグする場合に非常に複雑です。
    • IUrlHelper、タグ ヘルパー、HTML ヘルパー、アクションの結果などで使用される実装を提供します。

アドレス スキームの役割は、任意の条件によって、アドレスと一致するエンドポイント間の関連付けを作成することです。

  • エンドポイント名スキームでは、基本的な辞書検索が実行されます。
  • ルート値のスキームには、セット アルゴリズムの複雑な最良のサブセットがあります。

アンビエント値と明示的な値

ルーティングは、現在の要求から現在の要求 HttpContext.Request.RouteValues のルート値にアクセスします。 現在の要求に関連付けられている値は、アンビエント値と呼ばれます。 このドキュメントでは、わかりやすくするために、メソッドに渡されるルート値を明示的な値と呼びます。

次の例では、アンビエント値と明示的な値を示しています。 ここでは、現在の要求のアンビエント値と明示的な値を提供します。

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

上記のコードでは次の操作が行われます。

  • /Widget/Index/17 を返します。
  • DI を介して LinkGenerator を取得します。

次のコードでは、明示的な値のみ提供されており、アンビエント値は提供されていません。

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

前述のメソッドでは /Home/Subscribe/17 が返されます。

WidgetController の次のコードでは、/Widget/Subscribe/17 が返されます。

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

次のコードは、現在の要求のアンビエント値と明示的な値からコントローラーを提供します。

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

上のコードでは以下の操作が行われます。

  • /Gadget/Edit/17 が返されます。
  • UrlIUrlHelper を取得します。
  • Action が、アクション メソッドの絶対パスを使用して URL を生成します。 URL には、指定した action 名と route 値が含まれます。

次のコードでは、現在の要求のアンビエント値と明示的な値を提供します。

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

前述のコードでは、Edit Razor ページに次のページ ディレクティブが含まれている場合に、url/Edit/17 に設定します。

@page "{id:int}"

[編集] ページに "{id:int}" ルート テンプレートが含まれない場合は、url/Edit?id=17 になります。

MVC の IUrlHelper の動作により、ここで説明するルールに加えて、複雑なレイヤーが追加されます。

  • IUrlHelper は、常に現在の要求からルート値をアンビエント値として提供します。
  • IUrlHelper.Action は、開発者がオーバーライドしない場合を除き、常に現在の actioncontroller ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の page ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の handler ルート値を明示的な値として null にオーバーライドします。

ユーザーは、MVC が独自のルールに従っていないように見えるため、アンビエント値の動作の詳細にしばしば驚きます。 これまでの経緯および互換性の理由により、actioncontrollerpagehandler などの特定のルート値には、独自の特殊な動作があります。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage の同等の機能では、互換性のために IUrlHelper のこの異常と同じ動作が行われます。

URL の生成処理

一連の候補エンドポイントが見つかると、URL の生成アルゴリズムでは次が実行されます。

  • エンドポイントが反復処理されます。
  • 最初に成功した結果が返されます。

このプロセスの最初の手順はルート値の無効化と呼ばれます。 ルート値の無効化は、アンビエント値からどのルート値を使用する必要があり、無視する必要があるかをルーティングが決定するプロセスです。 各アンビエント値が検討され、明示的な値と組み合わされるか、または無視されます。

アンビエント値の役割について一番わかりやすい考え方は、一部の一般的なケースでアプリケーション開発者の入力作業が省かれるということです。 従来、アンビエント値が役に立つシナリオは MVC に関連しています。

  • 同じコントローラー内の別のアクションにリンクする場合、コントローラー名を指定する必要はありません。
  • 同じ領域内の別のコントローラーにリンクする場合、領域名を指定する必要はありません。
  • 同じアクション メソッドにリンクする場合は、ルート値を指定する必要はありません。
  • アプリの別の部分にリンクする場合は、アプリのその部分には意味のないルート値は引き継ぎません。

null を返す LinkGenerator または IUrlHelper の呼び出しは、通常、ルート値の無効化について理解していないことが原因で発生します。 ルート値の無効化のトラブルシューティングを行うには、さらにルート値を明示的に指定して、これにより問題が解決されるかどうかを確認します。

ルート値の無効化は、アプリの URL スキームが階層的であり、階層が左から右に形成されていることを前提として機能します。 基本的なコントローラー ルート テンプレート {controller}/{action}/{id?} について考えてみましょう。これが実際にどのように動作するかを直感的に理解できます。 値に対する変更により、右側に表示されるすべてのルート値が無効化されます。 これには、階層に関する前提が反映されています。 アプリに id のアンビエント値があり、操作によって controller に対して異なる値が指定された場合、

  • {controller}{id?} の左側にあるため、id は再利用されません。

この原則を示すいくつかの例を次に示します。

  • 明示的な値に id の値が含まれている場合、id のアンビエント値は無視されます。 controlleraction のアンビエント値を使用できます。
  • 明示的な値に action の値が含まれている場合、action のアンビエント値はすべて無視されます。 controller のアンビエント値を使用できます。 action の明示的な値が action のアンビエント値と異なる場合、id 値は使用されません。 action の明示的な値が action のアンビエント値と同じ場合、id 値を使用できます。
  • 明示的な値に controller の値が含まれている場合、controller のアンビエント値はすべて無視されます。 controller の明示的な値が controller のアンビエント値と異なる場合、actionid の値は使用されません。 controller の明示的な値が controller のアンビエント値と同じ場合、actionid の値を使用できます。

このプロセスは、属性ルートと専用規則ルートが存在することでさらに複雑になります。 {controller}/{action}/{id?} などのコントローラーの規則ルートでは、ルート パラメーターを使用して階層が指定されます。 コントローラーと Razor Pages に対する専用規則ルート属性ルートの場合、

  • ルート値の階層があります。
  • テンプレートには表示されません。

このような場合は、URL の生成によって必要な値の概念が定義されます。 コントローラーおよび Razor Pages によって作成されたエンドポイントには、ルート値の無効化を機能させるために必要な値が指定されています。

ルート値の無効化アルゴリズムの詳細は次のとおりです。

  • 必要な値の名前がルート パラメーターと組み合わされ、左から右に処理されます。
  • 各パラメーターについて、アンビエント値と明示的な値が比較されます。
    • アンビエント値と明示的な値が同じ場合、プロセスは続行されます。
    • アンビエント値が存在し、明示的な値が存在しない場合は、URL を生成するときにアンビエント値が使用されます。
    • アンビエント値が存在せず、明示的な値が存在する場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。
    • アンビエント値と明示的な値が存在し、2 つの値が異なる場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。

この時点で、URL の生成操作はルート制約を評価する準備ができています。 許容可能な値のセットがパラメーターの既定値と組み合わされ、制約に提供されます。 すべての制約について合格した場合、操作が続行されます。

次に、許容可能な値を使用してルート テンプレートを展開できます。 ルート テンプレートは次のように処理されます。

  • 左から右。
  • 各パラメーターに、許容可能な値が代入されます。
  • 次の特殊なケースがあります。
    • 許容可能な値がなく、パラメーターに既定値がある場合は、既定値が使用されます。
    • 許容可能な値がなく、パラメーターが省略可能な場合は、処理が続行されます。
    • 存在しない省略可能なパラメーターの右側にあるルート パラメーターのいずれかに値がある場合、操作は失敗します。
    • 連続する既定値パラメーターと省略可能なパラメーターは、可能な場合、折りたたまれています。

ルートのセグメントと一致しない明示的に指定された値は、クエリ文字列に追加されます。 次の表は、ルート テンプレート {controller}/{action}/{id?} の使用時の結果をまとめたものです。

アンビエント値 明示的な値 結果
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

ルート値の無効化に関する問題

次のコードは、ルーティングでサポートされていない URL 生成スキームの例を示しています。

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

上記のコードでは、culture ルート パラメーターがローカライズに使用されています。 culture パラメーターを常にアンビエント値として許容されるようにすることが望まれます。 しかし、必要な値の動作方法が理由で、culture パラメーターはアンビエント値として許容されません。

  • "default" ルート テンプレートでは、culture ルート パラメーターは controller の左側にあるため、controller を変更しても culture は無効になりません。
  • "blog" ルート テンプレートでは、culture ルート パラメーターは controller の右側にあると見なされ、必要な値に表示されます。

LinkParser で URL パスを解析する

LinkParser クラスは、URL パスをルート値のセットに解析するためのサポートを追加します。 ParsePathByEndpointName メソッドは、エンドポイント名と URL パスを受け取り、URL パスから抽出された一連のルート値を返します。

次のコントローラーの例では、GetProduct アクションは api/Products/{id} のルート テンプレートを使用し、GetProduct という Name を持ちます。

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

同じコントローラー クラス内で、AddRelatedProduct アクションには URL パス pathToRelatedProduct が必要です。これは、クエリ文字列パラメーターとして指定できます。

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

前の例では、AddRelatedProduct アクションによって URL パスから id ルート値が抽出されます。 たとえば、/api/Products/1 の URL パスが指定されている場合、relatedProductId 値は 1 に設定されます。 この方法により、API のクライアントは、リソースを参照するときに URL パスを使用できます。そのような URL の構造に関する知識は必要ありません。

エンドポイント メタデータを構成する

次のリンクでは、エンドポイント メタデータを構成する方法に関する情報を提供しています。

RequireHost とルートが一致するホスト

RequireHost では、指定したホストが必要であるという制約がルートに適用されます。 RequireHost または [Host] パラメーターには、以下を指定できます。

  • ホスト: www.domain.com。任意のポートの www.domain.com と一致します。
  • ホストとワイルドカード: *.domain.com。任意のポートの www.domain.comsubdomain.domain.com、または www.subdomain.domain.com と一致します。
  • ポート: *:5000。任意のホストのポート 5000 と一致します。
  • ホストとポート: www.domain.com:5000 または *.domain.com:5000。ホストとポートと一致します。

RequireHost または [Host] を使用して、複数のパラメーターを指定できます。 制約は、いずれかのパラメーターに対して有効なホストと一致します。 たとえば、[Host("domain.com", "*.domain.com")]domain.comwww.domain.com、および subdomain.domain.com と一致します。

次のコードでは、RequireHost を使用して、指定したホストをルートに対して要求します。

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

次のコードでは、コントローラー上の [Host] 属性を使用して、指定したホストのいずれかを要求します。

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

[Host] 属性がコントローラーとアクション メソッドの両方に適用される場合は、次のようになります。

  • アクションの属性が使用されます。
  • コントローラーの属性は無視されます。

ルーティングに関するパフォーマンス ガイダンス

アプリにパフォーマンス上の問題がある場合、多くの場合ルーティングが問題として疑われます。 ルーティングが疑われる理由は、コントローラーや Razor Pages などのフレームワークにより、フレームワーク内で費やされた時間がログ メッセージで報告されるためです。 コントローラーによって報告された時間と要求の合計時間の間に大きな違いがある場合、次のようになります。

  • 開発者は、問題の発生源としてアプリ コードを排除します。
  • ルーティングが原因であると考えるのが一般的です。

ルーティングは、数千のエンドポイントを使用してパフォーマンス テストされています。 一般的なアプリでは、大きすぎるだけでパフォーマンスの問題が発生する可能性はほとんどありません。 ルーティングのパフォーマンス低下の最も一般的な根本原因は、通常、正しく動作していないカスタム ミドルウェアです。

次のコード サンプルは、遅延の原因を絞り込むための基本的な手法を示したものです。

var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

ルーティングの時間は次のように計ります。

  • 各ミドルウェアを、上記のコードに示されている時間を計るミドルウェアのコピーでインターリーブします。
  • 計られた時間データをコードと関連付けるための一意の識別子を追加します。

これは、遅延が 10ms を超えるなど顕著な場合に絞り込むための基本的な方法です。 Time 1 から Time 2 を引くことで、UseRouting ミドルウェア内で費やされた時間を報告します。

次のコードでは、前の時間を計るコードに対して、よりコンパクトなアプローチを使用します。

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");

潜在的にコストが高いルーティング機能

次の一覧に、基本的なルート テンプレートと比べて比較的コストが高いルーティング機能についての洞察を示します。

  • 正規表現: 複雑な正規表現を作成すること、つまり少量の入力で長い実行時間を実現することができます。
  • 複雑なセグメント ({x}-{y}-{z}):
    • 通常の URL パス セグメントを解析するよりもかなりコストがかかります。
    • より多くの部分文字列が割り当てられることになります。
  • 同期データ アクセス: 多くの複雑なアプリでは、ルーティングの一部としてデータベースにアクセスします。 非同期的な MatcherPolicyEndpointSelectorContext などの拡張ポイントを使用します。

大規模なルート テーブルに関するガイダンス

既定では ASP.NET Core によって、メモリを CPU 時間とトレードするルーティング アルゴリズムが使用されます。 その効果として、ルート照合の時間が、照合するパスの長さにのみ左右され、ルートの数には左右されなくなります。 ただし、アプリに多数 (何千個) のルートがあり、大量の変数のプレフィックスがルートにあるようなケースでは、この方式が問題となる可能性があります。 たとえば、{parameter}/some/literal など、ルートの前方のセグメントにパラメーターが存在する場合があります。

以下のケースを除き、この方式が問題となる状況にアプリが陥る可能性は極めて低いです。

  • このパターンを使用するアプリに多数のルートがある。
  • アプリに多数のルートがある。

アプリで大規模なルート テーブルの問題が発生しているかどうかを判断する方法

  • 次の 2 つの症状がないかどうかを確認します。
    • 最初の要求でアプリの起動が遅い。
      • これは必須条件ですが、これだけでは不十分です。 ルート以外のさまざまな問題でも、アプリの起動が遅くなることがあります。 以下の条件を確認して、当該の状況がアプリで発生しているかどうかを正確に判断します。
    • アプリが起動時に大量のメモリを消費し、メモリ ダンプに多数の Microsoft.AspNetCore.Routing.Matching.DfaNode インスタンスが示されている。

この問題に対処する方法

ルートに適用可能ないくつかの手法や最適化を使用して、このシナリオを大幅に改善できます。

  • 可能な場合に、{parameter:int}{parameter:guid}{parameter:regex(\\d+)} などのルート制約をパラメーターに適用します。
    • これにより、ルーティング アルゴリズムで、照合に使用される構造を内部的に最適化し、使用メモリを大幅に削減できるようになります。
    • ほとんどの場合、許容できる動作に戻すのにこれで十分です。
  • テンプレート内の後方のセグメントにパラメーターを移動するように、ルートを変更します。
    • これにより、パスの指定されたエンドポイントに一致する可能性がある "パス" の数が減少します。
  • 動的ルートを使用し、コントローラー/ページへのマッピングを動的に実行します。
    • これは、MapDynamicControllerRoute および MapDynamicPageRoute を使用して実施できます。

ライブラリ作成者向けのガイダンス

このセクションでは、ルーティングを基盤とするライブラリ作成者向けのガイダンスを示します。 これらの詳細情報は、アプリの開発者が、ルーティングを拡張するライブラリとフレームワークを使用して優れたエクスペリエンスを実現できるようにすることを目的としています。

エンドポイントを定義する

URL 照合にルーティングを使用するフレームワークを作成するには、まず UseEndpoints の上に構築されるユーザー エクスペリエンスを定義します。

IEndpointRouteBuilder の上に構築します。 これにより、ユーザーは、他の ASP.NET Core 機能と混同せずにフレームワークを構成できます。 すべての ASP.NET Core テンプレートには、ルーティングが含まれます。 ルーティングが存在し、ユーザーになじみのあるものとします。

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

IEndpointConventionBuilder を実装する MapMyFramework(...) の呼び出しから、シールドの具象型を返します。 ほとんどのフレームワーク Map... メソッドは、このパターンに従います。 IEndpointConventionBuilder インターフェイスは、

  • メタデータを構成できます。
  • さまざまな拡張メソッドの対象とされています。

独自の型を宣言すると、独自のフレームワーク固有の機能をビルダーに追加できます。 フレームワークで宣言されたビルダーをラップし、呼び出しを転送するのは問題ありません。

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

独自の EndpointDataSource を作成することを検討しますEndpointDataSource は、エンドポイントのコレクションを宣言および更新するための低レベルのプリミティブです。 EndpointDataSource は、コントローラーと Razor Pages によって使用される強力な API です。

ルーティング テストには、更新されていないデータ ソースの基本的な例があります。

既定では、EndpointDataSource の登録を試行しないでくださいUseEndpoints にフレームワークを登録するようユーザーに要求してください。 ルーティングの原理では、既定では何も含まれておらず、UseEndpoints がエンドポイントを登録する場所です。

ルーティング統合ミドルウェアを作成する

メタデータ型をインターフェイスとして定義することを検討します

クラスおよびメソッドの属性としてメタデータ型を使用できるようにします

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

コントローラーや Razor Pages などのフレームワークでは、型およびメソッドへのメタデータ属性の適用がサポートされています。 メタデータ型を宣言する場合:

  • 属性としてアクセスできるようにします。
  • ほとんどのユーザーが属性の適用に精通しています。

メタデータ型をインターフェイスとして宣言すると、柔軟性の高いレイヤーがさらに追加されます。

  • インターフェイスが構成可能です。
  • 開発者は、複数のポリシーを結合する独自の型を宣言できます。

次の例に示すように、メタデータをオーバーライドできるようにします

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

これらのガイドラインに従う最善の方法は、マーカーのメタデータを定義しないようにすることです。

  • メタデータ型が存在するかどうかを確認するだけで終わらせません。
  • メタデータのプロパティを定義し、プロパティを確認します。

メタデータ コレクションが順序付けされ、優先順位によるオーバーライドがサポートされます。 コントローラーの場合、アクション メソッドのメタデータが最も限定的です。

ルーティングを使用する場合もしない場合もミドルウェアが役立つようにします

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

このガイドラインの例として、UseAuthorization ミドルウェアを考えてみましょう。 この承認ミドルウェアを使用すると、フォールバック ポリシーを渡すことができます。 フォールバック ポリシーは、指定されている場合、次の両方に適用されます。

  • 指定されたポリシーのないエンドポイント。
  • エンドポイントに一致しない要求。

これにより、承認ミドルウェアはルーティングのコンテキストの外部で役に立ちます。 承認ミドルウェアは、従来のミドルウェア プログラミングに使用できます。

デバッグ診断

詳細なルーティング診断出力を行うには、Logging:LogLevel:MicrosoftDebug に設定してください。 開発環境では、appsettings.Development.json でログ レベルを次のように設定します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

その他のリソース

ルーティングの役割は、受信した HTTP 要求を照合し、それらの要求をアプリの実行可能なエンドポイントにディスパッチすることです。 エンドポイントは、アプリの実行可能な要求処理コードの単位です。 エンドポイントはアプリで定義され、アプリの起動時に構成されます。 エンドポイントの照合プロセスでは、要求の URL から値を抽出し、それらの値を要求の処理に提供できます。 アプリからのルート情報を使用して、ルーティングでエンドポイントにマップする URL を生成することもできます。

アプリでは、次のものを使用してルーティングを構成できます。

このドキュメントでは、ASP.NET Core のルーティングについて詳しく説明します。 ルーティングの構成については、以下を参照してください。

このドキュメントで説明されているエンドポイント ルーティング システムは、ASP.NET Core 3.0 以降に適用されます。 IRouter に基づく以前のルーティング システムについては、次のいずれかの方法を使用して ASP.NET Core 2.1 バージョンを選択してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

このドキュメントのダウンロード サンプルは、特定の Startup クラスによって有効になります。 特定のサンプルを実行するには、目的の Startup クラスを呼び出すように Program.cs を変更します。

ルーティングの基本

すべての ASP.NET Core テンプレートには、生成されたコードでのルーティングが含まれます。 ルーティングは、Startup.Configureミドルウェア パイプラインに登録されます。

次のコードでは、ルーティングの基本的な例を示します。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

ルーティングでは、UseRoutingUseEndpoints によって登録されたミドルウェアのペアを使用します。

  • UseRouting では、ルートの照合がミドルウェア パイプラインに追加されます。 このミドルウェアによって、アプリで定義されているエンドポイントのセットが調べられ、要求に基づいて最適な一致が選択されます。
  • UseEndpoints では、エンドポイントの実行がミドルウェア パイプラインに追加されます。 選択されたエンドポイントに関連付けられているデリゲートが実行されます。

前の例には、MapGet メソッドを使用する "コードへのルーティング" エンドポイントが 1 つ含まれます。

  • HTTP GET 要求がルート URL / に送信された場合:
    • 示されている要求デリゲートが実行されます。
    • Hello World! が HTTP 応答に書き込まれます。 既定では、ルート URL /https://localhost:5001/ です。
  • 要求メソッドが GET ではない場合、またはルート URL が / ではない場合は、一致するルートはなく、HTTP 404 が返されます。

エンドポイント

エンドポイントを定義するには、MapGet メソッドが使用されます。 エンドポイントとは、次のようなものです。

  • URL と HTTP メソッドを一致させることによって選択できます。
  • デリゲートを実行することによって実行できます。

アプリによって一致させて実行できるエンドポイントは、UseEndpoints で構成します。 たとえば、MapGetMapPost、および類似のメソッドでは、要求のデリゲートがルーティング システムに接続されます。 他のメソッドを使用して、ASP.NET Core フレームワークの機能をルーティング システムに接続できます。

次の例では、より高度なルート テンプレートによるルーティングを示します。

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/hello/{name:alpha}", async context =>
    {
        var name = context.Request.RouteValues["name"];
        await context.Response.WriteAsync($"Hello {name}!");
    });
});

文字列 /hello/{name:alpha} は、ルート テンプレートです。 これは、エンドポイントの一致方法を構成するために使用されます。 この場合、テンプレートは次のものと一致します。

  • /hello/Ryan のような URL
  • /hello/ で始まり、その後に一連の英字が続く任意の URL パス。 :alpha では、英字のみと一致するルート制約が適用されます。 ルート制約については、このドキュメントで後ほど説明します。

URL パスの 2 番目のセグメント {name:alpha} は次のようになります。

  • name パラメーターにバインドされます。
  • キャプチャされて HttpRequest.RouteValues に格納されます。

このドキュメントで説明されているエンドポイント ルーティング システムは、ASP.NET Core 3.0 での新機能です。 ただし、ASP.NET Core のすべてのバージョンで、同じルート テンプレート機能とルート制約のセットがサポートされます。

次の例では、正常性チェックと承認を使用するルーティングを示します。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

コードのコメントを英語以外の言語に翻訳し表示したい場合、こちらの GitHub ディスカッション イシューにてお知らせください。

上の例では、次の方法が示されています。

  • ルーティングで承認ミドルウェアを使用できます。
  • エンドポイントを使用して、承認動作を構成できます。

MapHealthChecks の呼び出しにより、正常性チェック エンドポイントが追加されます。 この呼び出しに RequireAuthorization をチェーンすると、エンドポイントに承認ポリシーがアタッチされます。

UseAuthenticationUseAuthorization を呼び出すと、認証ミドルウェアと承認ミドルウェアが追加されます。 これらのミドルウェアは、次のことができるように UseRoutingUseEndpoints の間に配置されます。

  • UseRouting によって選択されたエンドポイントを確認します。
  • UseEndpoints によってエンドポイントにディスパッチされる前に、承認ポリシーを適用します。

エンドポイントのメタデータ

前の例には 2 つのエンドポイントがありますが、承認ポリシーがアタッチされているのは正常性チェック エンドポイントだけです。 要求が正常性チェック エンドポイント /healthz と一致した場合、承認チェックが実行されます。 これは、エンドポイントに追加のデータをアタッチできることを示しています。 この追加データは、エンドポイントのメタデータと呼ばれます。

  • メタデータは、ルーティング対応ミドルウェアによって処理できます。
  • メタデータには、任意の .NET 型を使用できます。

ルーティングの概念

ルーティング システムは、ミドルウェア パイプラインを基にして、強力なエンドポイント概念を追加することにより、構築されています。 エンドポイントは、ルーティング、承認、および任意の数の ASP.NET Core システムに関して相互に独立している、アプリの機能の単位を表します。

ASP.NET Core エンドポイントの定義

ASP.NET Core エンドポイントとは次のようなものです。

  • 実行可能: RequestDelegate を持っています。
  • 拡張可能: Metadata コレクションを持っています。
  • Selectable: 必要に応じて、ルーティング情報を持ちます。
  • 列挙可能: エンドポイントのコレクションの一覧は、DI から EndpointDataSource を取得することによって得られます。

次のコードでは、エンドポイントを取得し、現在の要求と一致するものを検査する方法を示します。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.Use(next => context =>
    {
        var endpoint = context.GetEndpoint();
        if (endpoint is null)
        {
            return Task.CompletedTask;
        }
        
        Console.WriteLine($"Endpoint: {endpoint.DisplayName}");

        if (endpoint is RouteEndpoint routeEndpoint)
        {
            Console.WriteLine("Endpoint has route pattern: " +
                routeEndpoint.RoutePattern.RawText);
        }

        foreach (var metadata in endpoint.Metadata)
        {
            Console.WriteLine($"Endpoint has metadata: {metadata}");
        }

        return Task.CompletedTask;
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

エンドポイントが選択されている場合は、HttpContext から取得できます。 そのプロパティを検査できます。 エンドポイント オブジェクトは不変であり、作成後に変更することはできません。 最も一般的なエンドポイントの型は RouteEndpoint です。 RouteEndpoint には、ルーティング システムによって選択されるための情報が含まれています。

上のコードでは、app.Use によってインライン ミドルウェアが構成されます。

次のコードでは、パイプラインで app.Use が呼び出される場所によっては、エンドポイントが存在しない場合があることを示します。

// Location 1: before routing runs, endpoint is always null here
app.Use(next => context =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseEndpoints(endpoints =>
{
    // Location 3: runs when this endpoint matches
    endpoints.MapGet("/", context =>
    {
        Console.WriteLine(
            $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
        return Task.CompletedTask;
    }).WithDisplayName("Hello");
});

// Location 4: runs after UseEndpoints - will only run if there was no match
app.Use(next => context =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

前のサンプルでは、エンドポイントが選択されているかどうかを表示する Console.WriteLine ステートメントが追加されています。 わかりやすくするため、このサンプルでは、指定された / エンドポイントに表示名が割り当てられています。

このコードを / の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

このコード他の URL で実行すると、次のように表示されます。

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

この出力は次のことを示しています。

  • UseRouting が呼び出される前は、エンドポイントは常に null になっています。
  • 一致が見つかった場合、エンドポイントは UseRoutingUseEndpoints の間で null 以外の値になります。
  • 一致が見つかると、UseEndpoints ミドルウェアはターミナルです。 ターミナル ミドルウェアについては、このドキュメントで後ほど定義します。
  • UseEndpoints の後のミドルウェアは、一致が検出されなかった場合にのみ実行されます。

UseRouting ミドルウェアでは、SetEndpoint メソッドを使用して、エンドポイントが現在のコンテキストにアタッチされます。 UseRouting ミドルウェアをカスタム ロジックに置き換えることができ、その場合でもエンドポイントを使用する利点を得られます。 エンドポイントはミドルウェアのような低レベルのプリミティブであり、ルーティングの実装には結合されません。 ほとんどのアプリでは、UseRouting をカスタム ロジックに置き換える必要はありません。

UseEndpoints ミドルウェアは、UseRouting ミドルウェアと連携して使用するように設計されています。 エンドポイントを実行するためのコア ロジックは複雑ではありません。 GetEndpoint を使用してエンドポイントを取得し、その RequestDelegate プロパティを呼び出します。

次のコードでは、ミドルウェアがルーティングに与える影響またはルーティングに対応する方法を示します。

public class IntegratedMiddlewareStartup
{ 
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Location 1: Before routing runs. Can influence request before routing runs.
        app.UseHttpMethodOverride();

        app.UseRouting();

        // Location 2: After routing runs. Middleware can match based on metadata.
        app.Use(next => context =>
        {
            var endpoint = context.GetEndpoint();
            if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit
                                                                            == true)
            {
                Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
            }

            return next(context);
        });

        app.UseEndpoints(endpoints =>
        {         
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello world!");
            });

            // Using metadata to configure the audit policy.
            endpoints.MapGet("/sensitive", async context =>
            {
                await context.Response.WriteAsync("sensitive data");
            })
            .WithMetadata(new AuditPolicyAttribute(needsAudit: true));
        });

    } 
}

public class AuditPolicyAttribute : Attribute
{
    public AuditPolicyAttribute(bool needsAudit)
    {
        NeedsAudit = needsAudit;
    }

    public bool NeedsAudit { get; }
}

前の例では、2 つの重要な概念が示されています。

  • ミドルウェアは、UseRouting の前に実行して、ルーティングの動作に使用されるデータを変更できます。
  • ミドルウェアは、UseRoutingUseEndpoints の間で実行して、エンドポイントが実行される前にルーティングの結果を処理できます。
    • UseRoutingUseEndpoints の間で実行されるミドルウェア:
      • 通常、メタデータを検査してエンドポイントを認識します。
      • 多くの場合、UseAuthorizationUseCors によって実行されるセキュリティに関する決定を行います。
    • ミドルウェアとメタデータを組み合わせることで、エンドポイントごとにポリシーを構成できます。

上のコードでは、エンドポイントごとのポリシーをサポートするカスタム ミドルウェアの例が示されています。 ミドルウェアによって、機密データへのアクセスの "監査ログ" がコンソールに書き込まれます。 AuditPolicyAttribute メタデータを使用して、エンドポイントを "監査する" ようにミドルウェアを構成できます。 このサンプルでは、機密としてマークされているエンドポイントのみが監査される "オプトイン" パターンを示します。 このロジックの逆を定義して、たとえば安全としてマークされていないすべてのものを監査することができます。 エンドポイント メタデータ システムは柔軟です。 ユース ケースに適したどのような方法でも、このロジックを設計できます。

前のサンプル コードは、エンドポイントの基本的な概念を示すことが意図されています。 サンプルは運用環境での使用は意図されていません。 より完全なバージョンの "監査ログ" ミドルウェアでは、次のことが行われます。

  • ファイルまたはデータベースにログを記録します。
  • ユーザー、IP アドレス、機密性の高いエンドポイントの名前などの詳細情報が追加されます。

コントローラーや SignalR などのクラスベースのフレームワークで簡単に使用できるように、監査ポリシー メタデータ AuditPolicyAttributeAttribute と定義されています。 "コードへのルーティング" を使用すると、次のようになります。

  • メタデータがビルダー API にアタッチされます。
  • エンドポイントを作成するとき、クラス ベースのフレームワークに、対応するメソッドとクラスのすべての属性が組み込まれます。

メタデータの型に対するベスト プラクティスは、インターフェイスまたは属性として定義することです。 インターフェイスと属性では、コードを再利用できます。 メタデータ システムは柔軟であり、どのような制限もありません。

ターミナル ミドルウェアとルーティングの比較

次のコード サンプルでは、ミドルウェアの使用とルーティングの使用が比較されています。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Approach 1: Writing a terminal middleware.
    app.Use(next => async context =>
    {
        if (context.Request.Path == "/")
        {
            await context.Response.WriteAsync("Hello terminal middleware!");
            return;
        }

        await next(context);
    });

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // Approach 2: Using routing.
        endpoints.MapGet("/Movie", async context =>
        {
            await context.Response.WriteAsync("Hello routing!");
        });
    });
}

Approach 1: で示されているミドルウェアのスタイルは、ターミナル ミドルウェアです。 ターミナル ミドルウェアと呼ばれるのは、照合操作を実行するためです。

  • 前のサンプルの照合操作は、ミドルウェアの場合は Path == "/"、ルーティングの場合は Path == "/Movie" です。
  • 照合が成功すると、next ミドルウェアを呼び出すのではなく、一部の機能を実行して戻ります。

ターミナル ミドルウェアと呼ばれるのは、検索を終了し、いくつかの機能を実行してから制御を返すためです。

ターミナル ミドルウェアとルーティングの比較:

  • どちらの方法でも、処理パイプラインを終了できます。
    • ミドルウェアでは、next を呼び出すのではなく、戻ることによってパイプラインが終了されます。
    • エンドポイントは常にターミナルです。
  • ターミナル ミドルウェアを使用すると、パイプライン内の任意の場所にミドルウェアを配置できます。
    • エンドポイントは、UseEndpoints の位置で実行されます。
  • ターミナル ミドルウェアでは、任意のコードを使用してミドルウェアが一致するかどうかを判定できます。
    • カスタム ルート一致コードは、冗長で、正しく記述するのが困難な場合があります。
    • ルーティングでは、一般的なアプリに対して簡単なソリューションが提供されます。 ほとんどのアプリでは、カスタム ルート一致コードは必要ありません。
  • UseAuthorizationUseCors などのミドルウェアを使用したエンドポイント インターフェイス。
    • UseAuthorization または UseCors でターミナル ミドルウェアを使用するには、承認システムとの手動インターフェイスが必要です。

エンドポイント では、次の両方が定義されます。

  • 要求を処理するためのデリゲート。
  • 任意のメタデータのコレクション。 メタデータは、各エンドポイントにアタッチされている構成とポリシーに基づいて横断的な関心事を実装するために使用されます。

ターミナル ミドルウェアは効果的なツールになる可能性がありますが、次のものが必要です。

  • 大量のコーディングとテスト。
  • 必要なレベルの柔軟性を実現するための、他のシステムとの手作業による統合。

ターミナル ミドルウェアを作成する前に、ルーティングとの統合を検討してください。

Map または MapWhen と統合されている既存のターミナル ミドルウェアは、通常、ルーティング対応のエンドポイントにすることができます。 MapHealthChecks では、ルーターウェアのパターンが示されています。

  • IEndpointRouteBuilder で拡張メソッドを作成します。
  • CreateApplicationBuilder を使用して、入れ子になったミドルウェア パイプラインを作成します。
  • ミドルウェアを新しいパイプラインにアタッチします。 例では、 UseHealthChecksが使用されます。
  • RequestDelegate にミドルウェア パイプラインを Build します。
  • Map を呼び出し、新しいミドルウェア パイプラインを提供します。
  • 拡張メソッドから Map によって提供されるビルダー オブジェクトを返します。

次のコードでは、MapHealthChecks の使用方法を示します。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

前のサンプルでは、ビルダー オブジェクトを返すことが重要である理由が示されています。 ビルダー オブジェクトを返すことで、アプリ開発者はエンドポイントの承認などのポリシーを構成できます。 この例では、正常性チェック ミドルウェアと承認システムは直接統合されていません。

そのメタデータ システムは、ターミナル ミドルウェアを使用する機能拡張作成者によって、発生する問題に対応して作成されました。 各ミドルウェアで承認システムとの独自の統合を実装することには問題があります。

URL 一致

  • ルーティングによって受信要求がエンドポイントと照合されるプロセスです。
  • URL パスとヘッダーのデータに基づいています。
  • 要求内の任意のデータを考慮するように拡張できます。

実行されたルーティング ミドルウェアでは、Endpoint が設定され、現在の要求からの HttpContext要求機能に値がルーティングされます。

  • HttpContext.GetEndpoint を呼び出すと、エンドポイントが取得されます。
  • HttpRequest.RouteValues では、ルート値のコレクションが取得されます。

ルーティング ミドルウェアの後で実行されたミドルウェアでは、エンドポイントを調べて、アクションを実行することができます。 たとえば、承認ミドルウェアでは、エンドポイントのメタデータ コレクションに対し、承認ポリシーを問い合わせることができます。 要求処理パイプライン内のすべてのミドルウェアが実行された後、選択したエンドポイントのデリゲートが呼び出されます。

エンドポイント ルーティングのルーティング システムでは、配布に関するすべての決定が行われます。 ミドルウェアでは選択されたエンドポイントに基づいてポリシーが適用されるため、次のことが重要です。

  • ディスパッチまたはセキュリティ ポリシーの適用に影響を与える可能性のある決定は、ルーティング システム内で行われます。

警告

後方互換性のために、コントローラーまたは Razor Pages エンドポイント デリゲートが実行されると、それまでに実行された要求処理に基づいて、RouteContext.RouteData のプロパティが適切な値に設定されます。

RouteContext の種類は、今後のリリースでは古いものとしてマークされます。

  • RouteData.ValuesHttpRequest.RouteValues に移行します。
  • RouteData.DataTokens を移行して、エンドポイント メタデータから IDataTokensMetadata を取得します。

URL の照合は、構成可能な一連のフェーズで動作します。 各フェーズでの出力は一致のセットとなります。 一致のセットは、次のフェーズでさらに絞り込むことができます。 ルーティングの実装では、一致するエンドポイントの処理順序は保証されません。 一致の可能性のあるものは一度にすべて処理されます。 URL 照合フェーズは、次の順序で発生します。 ASP.NET Core:

  1. エンドポイントのセットおよびそれらのルート テンプレートに対して URL パスを処理し、すべての一致を収集します。
  2. 前のリストを取得し、ルート制約が適用されると失敗する一致を削除します。
  3. 前のリストを取得し、MatcherPolicy インスタンスのセットを失敗させる一致を削除します。
  4. EndpointSelector を使用して、前のリストから最終的な決定を行います。

エンドポイントのリストは、次の内容に従って優先度付けが行われます。

EndpointSelector に到達するまで、各フェーズで一致するすべてのエンドポイントが処理されます。 EndpointSelector は最後のフェーズです。 一致の中から最も優先度の高いエンドポイントが最適な一致として選択されます。 最適な一致と優先度が同じである一致が他にもある場合は、あいまい一致の例外がスローされます。

ルートの優先順位はより具体的なルート テンプレートに、より高い優先度が与えられることに基づいて算出されます。 たとえば、テンプレート /hello/{message} を検討してみます。

  • どちらも URL パス /hello と一致します。
  • /hello の方がより具体的であるため、優先度が高くなります。

一般に、ルートの優先順位は、実際に使用される URL スキームの種類として最適なものを選択するのに適しています。 Order は、あいまいさを避けるために必要な場合にのみ使用します。

拡張性の種類がルーティングによって指定されるため、あいまいなルートを事前にルーティング システムによって計算することはできません。 ルート テンプレート /{message:alpha}/{message:int} などの例を考えてみましょう。

  • alpha 制約を使用すると、アルファベット文字のみと一致します。
  • int 制約を使用すると、数値のみと一致します。
  • これらのテンプレートのルート優先順位は同じですが、この両方と一致する単一の URL はありません。
  • 起動時にルーティング システムからあいまいエラーが報告された場合、それによってこの有効なユース ケースはブロックされます。

警告

UseEndpoints 内での処理の順序は、ルーティングの動作には影響しませんが、例外が 1 つあります。 MapControllerRoute および MapAreaRoute では、それぞれが呼び出された順序に基づいて、それぞれのエンドポイントに順序値が自動的に割り当てられます。 これにより、ルーティング システムでより古いルーティング実装と同じ保証を提供しなくても、コントローラーの長時間の動作がシミュレートされます。

ルーティングの従来の実装では、ルートの処理順序に依存するルーティング拡張性を実装することができます。 ASP.NET Core 3.0 以降でのエンドポイントのルーティング:

  • ルートの概念がありません。
  • 順序付けが保証されません。 すべてのエンドポイントが一度に処理されます。

ルート テンプレートの優先順位とエンドポイントの選択順序

ルート テンプレートの優先順位とは、どれほど具体的であるかに基づいて、各ルート テンプレートに値を割り当てるシステムです。 ルート テンプレートの優先順位:

  • 一般的なケースでは、エンドポイントの順序を調整する必要はなくなります。
  • 一般的に期待されるルーティング動作との一致が試みられます。

たとえば、テンプレート /Products/List/Products/{id} について考えてみます。 URL パス /Products/List に対しては、/Products/List の方が /Products/{id} よりも適していると想定するのが妥当です。 このように言えるのは、リテラル セグメント /List がパラメーター セグメント /{id} よりも優先順位が高いと見なされるためです。

優先順位のしくみの詳細は、ルート テンプレートの定義方法と関連付けられています。

  • より多くのセグメントを持つテンプレートは、より具体的なものと見なされます。
  • リテラル テキストを含むセグメントは、パラメーター セグメントよりも具体的であると見なされます。
  • 制約が含まれるパラメーター セグメントは、それが含まれないものよりも具体的であると見なされます。
  • 複雑なセグメントは、制約を含むパラメーター セグメントと同じくらい具体的であると見なされます。
  • キャッチオール パラメーターは、まったく具体的ではありません。 キャッチオール ルートに関する重要な情報については、「ルート テンプレート参照」のキャッチオールを参照してください。

実際の値のリファレンスについては、GitHub 上のソース コードを参照してください。

URL 生成の概念

URL の生成:

  • ルーティングにおいて、一連のルート値に基づいて URL パスを作成するプロセスです。
  • エンドポイントとそれにアクセスする URL を論理的に分離できます。

エンドポイント ルーティングには、LinkGenerator API が含まれます。 LinkGeneratorDI から使用できるシングルトン サービスです。 LinkGenerator API は、実行中の要求のコンテキスト外で使用することができます。 Mvc.IUrlHelper と、タグ ヘルパー、HTML ヘルパー、アクション結果など、IUrlHelper に依存するシナリオでは LinkGenerator API を内部的に使用して、リンク生成機能が提供されます。

リンク ジェネレーターは、アドレスアドレス スキーム の概念に基づいています。 アドレス スキームは、リンク生成で考慮すべきエンドポイントを決定する方法です。 たとえば、コントローラーおよび Razor Pages からの、多くのユーザーに馴染みのあるルート名やルート値シナリオは、アドレス スキームとして実装されます。

リンク ジェネレーターでは、次の拡張メソッドを介して、コントローラーおよび Razor Pages にリンクできます。

これらのメソッドのオーバーロードでは、HttpContext を含む引数が受け入れられます。 これらのメソッドは Url.Action および Url.Page と機能的には同等ですが、柔軟性とオプションがさらに提供されます。

GetPath* メソッドは、絶対パスを含む URI を生成するという点で Url.Action および Url.Page に最も似ています。 GetUri* メソッドでは常に、スキームとホストを含む絶対 URI が生成されます。 HttpContext を受け入れるメソッドでは、実行中の要求のコンテキストで URI が生成されます。 実行中の要求からのアンビエント ルート値、URL ベース パス、スキーム、およびホストは、オーバーライドされない限り使用されます。

LinkGenerator はアドレスと共に呼び出されます。 URI の生成は、次の 2 つの手順で行われます。

  1. アドレスは、そのアドレスと一致するエンドポイントのリストにバインドされます。
  2. 各エンドポイントの RoutePattern は、指定された値と一致するルート パターンが見つかるまで評価されます。 結果の出力は、リンク ジェネレーターに指定された他の URI 部分と結合され、返されます。

LinkGenerator によって提供されるメソッドでは、すべての種類のアドレスの標準的なリンク生成機能がサポートされます。 リンク ジェネレーターを使用する最も便利な方法は、特定のアドレスの種類の操作を実行する拡張メソッドを使用することです。

拡張メソッド 説明
GetPathByAddress 指定された値に基づき、絶対パスを含む URI を生成します。
GetUriByAddress 指定された値に基づき、絶対 URI を生成します。

警告

LinkGenerator メソッド呼び出しによる次の影響に注意してください。

  • 受信要求の Host ヘッダーが確認されないアプリ構成では、GetUri* 拡張メソッドは注意して使用してください。 受信要求の Host ヘッダーが確認されていない場合、信頼されていない要求入力を、ビューまたはページの URI でクライアントに送り返すことができます。 すべての運用アプリで、Host ヘッダーを既知の有効な値と照らし合わせて確認するようにサーバーを構成することをお勧めします。

  • ミドルウェアで Map または MapWhen と組み合わせて、LinkGenerator を使用する場合は注意してください。 Map* では、実行中の要求の基本パスが変更され、リンク生成の出力に影響します。 すべての LinkGenerator API で基本パスを指定することができます。 リンク生成への Map* の影響を元に戻すための空の基本パスを指定してください。

ミドルウェアの例

次の例では、ミドルウェアで LinkGenerator API を使用して、商品をリストするアクション メソッドへのリンクを作成します。 リンク ジェネレーターは、クラスに挿入し、GenerateLink を呼び出すことで、アプリのどのクラスでも使用できます。

public class ProductsLinkMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var url = _linkGenerator.GetPathByAction("ListProducts", "Store");

        httpContext.Response.ContentType = "text/plain";

        await httpContext.Response.WriteAsync($"Go to {url} to see our products.");
    }
}

ルート テンプレート参照

{} 内のトークンでは、ルートが一致した場合にバインドされるルート パラメーターが定義されます。 1 つのルート セグメントに複数のルート パラメーターを定義できますが、各ルート パラメーターをリテラル値で区切る必要があります。 たとえば、{controller=Home}{action=Index} は有効なルートになりません。{controller}{action} の間にリテラル値がないためです。 ルート パラメーターには名前を付ける必要があります。付加的な属性を指定することもあります。

ルート パラメーター以外のリテラル テキスト ({id} など) とパス区切り / は URL のテキストに一致する必要があります。 テキスト照合は復号された URL のパスを基盤とし、大文字と小文字が区別されます。 リテラル ルート パラメーターの区切り記号 ({ または }) を照合するには、文字を繰り返して区切り記号をエスケープします。 たとえば、{{ または }} です。

アスタリスク * または二重アスタリスク**:

  • ルート パラメーターのプレフィックスとして使用して、URI の残りの部分にバインドすることができます。
  • キャッチオール パラメーターと呼ばれています。 blog/{**slug} の例を次に示します。
    • /blog で始まり、その後に任意の値が続く URI と一致します。
    • /blog に続く値は、slug ルート値に割り当てられます。

警告

ルーティングでバグが原因で、キャッチオール パラメーターがルートと正しく一致しない可能性があります。 このバグの影響を受けるアプリには、次の特性があります。

  • キャッチオール ルート (たとえば、{**slug}")
  • キャッチオール ルートが、一致すべき要求と一致しません。
  • 他のルートを削除すると、キャッチオール ルートが機能し始めます。

このバグが発生するケースの例については、GitHub のバグ 18677 および 16579 を参照してください。

このバグのオプトイン修正は .NET Core 3.1.301 SDK 以降に含まれています。 次のコードにより、このバグを修正する内部スイッチが設定されます。

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

キャッチオール パラメーターは空の文字列に一致することもあります。

キャッチオール パラメーターでは、パス区切り / 文字を含め、URL の生成にルートが使用されるときに適切な文字がエスケープされます。 たとえば、ルート値が { path = "my/path" } のルート foo/{*path} では、foo/my%2Fpath が生成されます。 エスケープされたスラッシュに注意してください。 パス区切り文字をラウンドトリップさせるには、** ルート パラメーター プレフィックスを使用します。 { path = "my/path" } のルート foo/{**path} では、foo/my/path が生成されます。

任意のファイル拡張子が付いたファイル名のキャプチャを試行する URL パターンには、追加の考慮事項があります。 たとえば、テンプレート files/{filename}.{ext?} について考えてみます。 filenameext の両方の値が存在するときに、両方の値が入力されます。 URL に filename の値だけが存在する場合、末尾の . は任意であるため、このルートは一致となります。 次の URL はこのルートに一致します。

  • /files/myFile.txt
  • /files/myFile

ルート パラメーターには、既定値 が含まれることがあります。パラメーター名の後に既定値を指定し、等号 (=) で区切ることで指定されます。 たとえば、{controller=Home} では、controller の既定値として Home が定義されます。 パラメーターの URL に値がない場合、既定値が使用されます。 ルート パラメーターは、パラメーター名の終わりに疑問符 (?) を追加することでオプションとして扱われます。 たとえば、id? のようにします。 省略可能な値と既定のルート パラメーターの違いは次のとおりです。

  • 既定値を持つルート パラメーターでは常に値が生成されます。
  • 省略可能なパラメーターには、要求 URL によって値が指定された場合にのみ値が含められます。

ルート パラメーターには、URL からバインドされるルート値に一致しなければならないという制約が含まれることがあります。 : と制約名をルート パラメーター名の後に追加すると、ルート パラメーターのインライン制約が指定されます。 その制約で引数が要求される場合、制約名の後にかっこ (...) で囲まれます。 複数の "インライン制約" を指定するには、別の : と制約名を追加します。

制約名と引数が IInlineConstraintResolver サービスに渡され、URL 処理で使用する IRouteConstraint のインスタンスが作成されます。 たとえば、ルート テンプレート blog/{article:minlength(10)} によって、制約 minlength と引数 10 が指定されます。 ルート制約の詳細とこのフレームワークによって指定される制約のリストについては、「ルート制約参照」セクションを参照してください。

ルート パラメーターには、パラメーター トランスフォーマーを指定することもできます。 パラメーター トランスフォーマーを指定すると、リンクを生成し、アクションおよびページを URL と一致させるときにパラメーターの値が変換されます。 制約と同様に、パラメーター トランスフォーマーをルート パラメーターにインラインで追加することができます。その場合、ルート パラメーター名の後に : とトランスフォーマー名を追加します。 たとえば、ルート テンプレート blog/{article:slugify} では、slugify トランスフォーマーが指定されます。 パラメーター トランスフォーマーの詳細については、「パラメーター トランスフォーマー参照」セクションを参照してください。

次の表に、ルート テンプレートの例とその動作を示します。

ルート テンプレート 一致する URI の例 要求 URI
hello /hello 単一パス /hello にのみ一致します。
{Page=Home} / 一致し、PageHome に設定されます。
{Page=Home} /Contact 一致し、PageContact に設定されます。
{controller}/{action}/{id?} /Products/List Products コントローラーと List アクションにマッピングされます。
{controller}/{action}/{id?} /Products/Details/123 Products コントローラーと Details アクションにマッピングされ、id は 123 に設定されます。
{controller=Home}/{action=Index}/{id?} / Home コントローラーと Index メソッドにマッピングされます。 id は無視されます。
{controller=Home}/{action=Index}/{id?} /Products Products コントローラーと Index メソッドにマッピングされます。 id は無視されます。

一般的に、テンプレートの利用が最も簡単なルーティングの手法となります。 ルート テンプレート以外では、制約と既定値も指定できます。

複雑なセグメント

複雑なセグメントは、リテラル区切り文字を右から左に最短一致の方法で照合することによって処理されます。 たとえば、[Route("/a{b}c{d}")] は複雑なセグメントです。 複雑なセグメントは、それらを適切に使用する上で理解する必要がある特定の方法で機能します。 このセクションの例では、パラメーター値の中に区切り文字が含まれていない場合にのみ、複雑なセグメントが本当にうまく機能する理由を示します。 より複雑なケースでは、regex を使用し、値を手動で抽出する必要があります。

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

これは、ルーティングがテンプレート /a{b}c{d} と URL パス /abcd を使用して実行するステップの概要です。 | は、アルゴリズムの動作を視覚化するために使用されます。

  • 最初のリテラル (右から左へ) は c です。 そこで、/abcd は右から検索され、/ab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /ab|c|d は中断したところから検索されて、a が見つかり、/|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • 残りのテキストも残りのルート テンプレートも存在しないため、これは一致となります。

同じテンプレート /a{b}c{d} と、URL パス /aabcd を使用した場合の否定の例を次に示します。 | は、アルゴリズムの動作を視覚化するために使用されます。 このケースは一致ではありませんが、同じアルゴリズムで説明します。

  • 最初のリテラル (右から左へ) は c です。 そこで、/aabcd は右から検索され、/aab|c|d となります。
  • ここで、右にあるすべてのもの (d) がルート パラメーター {d} と照合されます。
  • 次のリテラル (右から左へ) は a です。 そのため /aab|c|d は中断したところから検索されて、a が見つかり、/a|a|b|c|d となります。
  • ここで、右の値 (b) がルート パラメーター {b} と照合されます。
  • この時点で、テキスト a が残っていますが、アルゴリズムは解析するためのルート テンプレートを使い果たしたので、これは一致とはなりません。

照合アルゴリズムは最短一致のため、次のようになります。

  • 各ステップで可能な限りの最短のテキストに一致します。
  • パラメーター値の内部に区切り記号の値が表示されている場合は一致しません。

正規表現を使用すると、一致の動作をより細かく制御できます。

最長一致 (遅延一致 とも呼ばれる) を使用すると、可能な限り長い文字列と一致します。 最短一致は、可能な限り最短の文字列と一致します。

ルート制約参照

ルート制約は、受信 URL と一致し、URL パスがルート値にトークン化されたときに実行されます。 ルート制約では、通常、ルート テンプレート経由で関連付けられるルート値を調べ、値が許容できるかどうかを true または false で決定します。 一部のルート制約では、ルート値以外のデータを使用し、要求をルーティングできるかどうかが考慮されます。 たとえば、HttpMethodRouteConstraint はその HTTP Verb に基づいて要求を承認または却下します。 制約は、要求のルーティングとリンクの生成で使用されます。

警告

入力の検証には制約を使用しないでください。 入力の検証に制約を使用した場合、入力が無効だと "404 見つかりません" が返されます。 無効な入力の場合は、"400 要求が無効です" と適切なエラー メッセージが生成されます。 ルート制約は、特定のルートに対する入力の妥当性を検証するためではなく、似たようなルートの違いを明らかにするために使用されます。

次の表では、ルート制約の例とそれに求められる動作をまとめています。

制約 一致の例 メモ
int {id:int} 123456789-123456789 あらゆる整数に一致する
bool {active:bool} trueFALSE true または false に一致する。 大文字と小文字は区別されない
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm インバリアント カルチャの有効な DateTime 値に一致します。 前の警告を参照してください。
decimal {price:decimal} 49.99-1,000.01 インバリアント カルチャの有効な decimal 値に一致します。 前の警告を参照してください。
double {weight:double} 1.234-1,001.01e8 インバリアント カルチャの有効な double 値に一致します。 前の警告を参照してください。
float {weight:float} 1.234-1,001.01e8 インバリアント カルチャの有効な float 値に一致します。 前の警告を参照してください。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 有効な Guid 値に一致する
long {ticks:long} 123456789-123456789 有効な long 値に一致する
minlength(value) {username:minlength(4)} Rick 4 文字以上の文字列であることが必要
maxlength(value) {filename:maxlength(8)} MyFile 8 文字以内の文字列であることが必要
length(length) {filename:length(12)} somefile.txt 厳密に 12 文字の文字列であることが必要
length(min,max) {filename:length(8,16)} somefile.txt 8 文字以上、16 文字以内の文字列であることが必要
min(value) {age:min(18)} 19 18 以上の整数値であることが必要
max(value) {age:max(120)} 91 120 以下の整数値であることが必要
range(min,max) {age:range(18,120)} 91 18 以上、120 以下の整数値であることが必要
alpha {name:alpha} Rick 文字列は 1 つまたは複数のアルファベット文字で構成されることが必要 (a-z、大文字と小文字は区別されません)。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 文字列は正規表現と一致する必要があります。 正規表現の定義に関するヒントを参照してください。
required {name:required} Rick URL 生成中、非パラメーターが提示されるように強制する

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

1 のパラメーターには、複数の制約をコロンで区切って適用できます。 たとえば、次の制約では、パラメーターが 1 以上の整数値に制限されます。

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

警告

URL の妥当性を検証し、CLR 型に変換するルート制約では、常にインバリアント カルチャが使用されます。 たとえば、int または DateTime の CLR 型に変換される場合などです。 これらの制約では、URL のローカライズが不可であることが前提です。 フレームワークから提供されるルート制約がルート値に格納されている値を変更することはありません。 URL から解析されたルート値はすべて文字列として格納されます。 たとえば、float 制約はルート値を浮動小数に変換しますが、変換された値は、浮動小数に変換できることを検証するためにだけ利用されます。

正規表現の制約

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

正規表現は、regex(...) ルート制約を使用して、インライン制約として指定できます。 MapControllerRoute ファミリのメソッドでも、制約のオブジェクト リテラルを取ります。 この形式が使用されている場合、文字列値は正規表現として解釈されます。

次のコードでは、インラインで regex 制約が使用されています。

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
        context => 
        {
            return context.Response.WriteAsync("inline-constraint match");
        });
 });

次のコードでは、regex 制約の指定にオブジェクト リテラルが使用されています。

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "people",
        pattern: "People/{ssn}",
        constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
        defaults: new { controller = "People", action = "List", });
});

ASP.NET Core フレームワークでは、正規表現コンストラクターに RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant が追加されます。 これらのメンバーの詳細については、「RegexOptions」を参照してください。

正規表現では、ルーティングや C# 言語で使用されるものに似た区切り記号とトークンが使用されます。 正規表現トークンはエスケープする必要があります。 インライン制約で正規表現 ^\d{3}-\d{2}-\d{4}$ を使用するには、次のいずれかを使用します。

  • \ 文字列エスケープ文字をエスケープするには、文字列で指定した \ 文字を、C# ソース ファイル内の \\ 文字に置き換えます。
  • 逐語的文字列リテラル

ルーティング パラメーター区切り記号文字 ({}[]) をエスケープするには、表現の文字を二重にします (例: {{}}[[]])。 次の表に、正規表現とそれにエスケープを適用した後のものを示します。

正規表現 エスケープ適用後の正規表現
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

ルーティングで使用される正規表現は、多くの場合、^ 文字で始まり、これは文字列の開始位置と一致します。 この式は、多くの場合、$ 文字で終わり、文字列の末尾と一致します。 ^ 文字と $ 文字により、正規表現がルート パラメーター値全体に一致することが保証されます。 ^ 文字と $ 文字がなければ、意図に反し、正規表現は文字列内のあらゆる部分文字列に一致してしまいます。 下の表では、一致または不一致の理由を例を示し説明します。

正規表現 String 一致したもの コメント
[a-z]{2} hello はい サブ文字列の一致
[a-z]{2} 123abc456 はい サブ文字列の一致
[a-z]{2} mz はい 一致する表現
[a-z]{2} MZ はい 大文字と小文字の使い方が違う
^[a-z]{2}$ hello いいえ 上の ^$ を参照
^[a-z]{2}$ 123abc456 いいえ 上の ^$ を参照

正規表現構文の詳細については、.NET Framework 正規表現に関するページを参照してください。

既知の入力可能値の集まりにパラメーターを制限するには、正規表現を使用します。 たとえば、{action:regex(^(list|get|create)$)} の場合、action ルート値は listgetcreate とのみ照合されます。 制約ディクショナリに渡された場合、文字列 ^(list|get|create)$ で同じものになります。 既知の制約に一致しない、制約ディクショナリに渡された制約も、正規表現として扱われます。 既知の制約に一致しない、テンプレート内で渡される制約は、正規表現としては扱われません。

カスタム ルート制約

カスタム ルート制約は、IRouteConstraint インターフェイスを実装して作成できます。 IRouteConstraint インターフェイスには、Match が含まれています。これでは、制約が満たされている場合は true を返し、それ以外の場合は false を返します。

カスタム ルート制約は通常必要ありません。 カスタム ルート制約を実装する前に、モデル バインドなどの代替手段を検討してください。

ASP.NET Core の Constraints フォルダーには、制約を作成するための適切な例が用意されています。 たとえば、GuidRouteConstraint です。

カスタムの IRouteConstraint を使うには、サービス コンテナー内の ConstraintMap に、ルート制約の種類が登録されている必要があります。 ConstraintMap は、ルート制約キーを、その制約を検証する IRouteConstraint の実装にマッピングするディクショナリです。 アプリの ConstraintMap は、Startup.ConfigureServices で、services.AddRouting 呼び出しの一部として、または services.Configure<RouteOptions> を使って RouteOptions を直接構成することで、更新できます。 次に例を示します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
    });
}

上記の制約は、次のコードに適用されます。

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    // GET /api/test/3
    [HttpGet("{id:customName}")]
    public IActionResult Get(string id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // GET /api/test/my/3
    [HttpGet("my/{id:customName}")]
    public IActionResult Get(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

MyDisplayRouteInfoRick.Docs.Samples.RouteInfo NuGet パッケージによって提供され、ルート情報が表示されます。

MyCustomConstraint を実装することにより、ルート パラメーターに 0 が適用されなくなります。

class MyCustomConstraint : IRouteConstraint
{
    private Regex _regex;

    public MyCustomConstraint()
    {
        _regex = new Regex(@"^[1-9]*$",
                            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
                            TimeSpan.FromMilliseconds(100));
    }
    public bool Match(HttpContext httpContext, IRouter route, string routeKey,
                      RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out object value))
        {
            var parameterValueString = Convert.ToString(value,
                                                        CultureInfo.InvariantCulture);
            if (parameterValueString == null)
            {
                return false;
            }

            return _regex.IsMatch(parameterValueString);
        }

        return false;
    }
}

警告

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressions に入力を提供して、サービス拒否攻撃を行う可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

上記のコードでは次の操作が行われます。

  • ルートの {id} セグメントの 0 を禁止します。
  • カスタム制約を実装する基本的な例を示しています。 実稼働しているアプリでは使用しないでください。

次のコードは、0 を含む id が処理されないようにする優れた方法です。

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return ControllerContext.MyDisplayRouteInfo(id);
}

上記のコードには MyCustomConstraint アプローチに対し、次の利点があります。

  • カスタム制約が必要ありません。
  • ルート パラメーターに 0 が含まれている場合は、よりわかりやすいエラーが返されます。

パラメーター トランスフォーマー参照

パラメーター トランスフォーマー:

たとえば、Url.Action(new { article = "MyTestArticle" }) のルート パターン blog\{article:slugify} のカスタム slugify パラメーター トランスフォーマーでは、blog\my-test-article が生成されます。

IOutboundParameterTransformer の次の実装を見てみましょう。

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(), 
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}

ルート パターンでパラメーター トランスフォーマーを使用するには、これを Startup.ConfigureServicesConstraintMap を使用して構成します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

ASP.NET Core フレームワークでは、エンドポイントを解決する URI の変換にパラメーター トランスフォーマーを使用します。 たとえば、パラメーター トランスフォーマーでは、areacontrolleractionpage を照合するために使用されるルート値が変換されます。

routes.MapControllerRoute(
    name: "default",
    template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

上記のルート テンプレートでは、アクション SubscriptionManagementController.GetAll は URI /subscription-management/get-all と照合されます。 パラメーター トランスフォーマーでは、リンクを生成するために使用されるルート値は変更されません。 たとえば、Url.Action("GetAll", "SubscriptionManagement") では /subscription-management/get-all が出力されます。

ASP.NET Core には、生成されたルートでパラメーター トランスフォーマーを使用する API 規則があります。

URL 生成参照

このセクションには、URL の生成で実装するアルゴリズムの参照情報が含まれています。 実際には、URL 生成の最も複雑な例で、コントローラーまたは Razor Pages が使用されます。 詳細については、コントローラーでのルーティングに関するページを参照してください。

URL の生成プロセスは、LinkGenerator. GetPathByAddress、または類似のメソッドへの呼び出しで開始されます。 このメソッドは、アドレス、一連のルート値、およびオプションで HttpContext からの現在の要求に関する情報と共に渡されます。

まずは、アドレスを使用して、アドレスの型に一致する IEndpointAddressScheme<TAddress> を使用して、一連の候補のエンドポイントが解決されます。

アドレス スキームによって一連の候補が検出されると、URL の生成操作が成功するまで、エンドポイントは反復的に並べ替えられ、処理されます。 URL が生成される際には、あいまいさの確認は行われず、最初に返される結果が最終的な結果になります。

ログを使用した URL 生成のトラブルシューティング

URL の生成のトラブルシューティングを行う場合、まずは Microsoft.AspNetCore.Routing のログ記録レベルを TRACE に設定します。 LinkGenerator では、問題のトラブルシューティングに役立つ、処理に関する多くの詳細がログに記録されます。

URL 生成の詳細については、「URL 生成参照」を参照してください。

アドレス

アドレスとは、リンク ジェネレーターへの呼び出しを一連の候補エンドポイントにバインドするために使用する、URL 生成の概念です。

アドレスとは、次の 2 つの実装を既定で備えた拡張可能な概念です。

  • アドレスとして "エンドポイント名" (string) を使用します。
    • MVC のルート名と同様の機能があります。
    • IEndpointNameMetadata メタデータ型を使用します。
    • 指定された文字列を、登録されているすべてのエンドポイントのメタデータに対して解決します。
    • 複数のエンドポイントが同じ名前を使用している場合は、起動時に例外をスローします。
    • コントローラーと Razor Pages 以外で汎用的に使用する場合にお勧めします。
  • ルート値 (RouteValuesAddress) をアドレスとして使用すると、次のようになります。
    • コントローラーおよび Razor Pages での従来の URL 生成と同様の機能があります。
    • 拡張およびデバッグする場合に非常に複雑です。
    • IUrlHelper、タグ ヘルパー、HTML ヘルパー、アクションの結果などで使用される実装を提供します。

アドレス スキームの役割は、任意の条件によって、アドレスと一致するエンドポイント間の関連付けを作成することです。

  • エンドポイント名スキームでは、基本的な辞書検索が実行されます。
  • ルート値のスキームには、セット アルゴリズムの複雑な最良のサブセットがあります。

アンビエント値と明示的な値

ルーティングは、現在の要求から現在の要求 HttpContext.Request.RouteValues のルート値にアクセスします。 現在の要求に関連付けられている値は、アンビエント値と呼ばれます。 このドキュメントでは、わかりやすくするために、メソッドに渡されるルート値を明示的な値と呼びます。

次の例では、アンビエント値と明示的な値を示しています。 これでは、アンビエント値を現在の要求と明示的な値から提供します ({ id = 17, })。

public class WidgetController : Controller
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public IActionResult Index()
    {
        var url = _linkGenerator.GetPathByAction(HttpContext,
                                                 null, null,
                                                 new { id = 17, });
        return Content(url);
    }

上記のコードでは次の操作が行われます。

  • /Widget/Index/17 を返します。
  • DI を介して LinkGenerator を取得します。

次のコードでは、アンビエント値と明示的な値は提供されていません ({ controller = "Home", action = "Subscribe", id = 17, })。

public IActionResult Index2()
{
    var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
                                             new { id = 17, });
    return Content(url);
}

前述のメソッドでは /Home/Subscribe/17 が返されます。

WidgetController の次のコードでは、/Widget/Subscribe/17 が返されます。

var url = _linkGenerator.GetPathByAction("Subscribe", null,
                                         new { id = 17, });

次のコードは、現在の要求と明示的な値のアンビエント値 ({ action = "Edit", id = 17, }) からコントローラーを提供します。

public class GadgetController : Controller
{
    public IActionResult Index()
    {
        var url = Url.Action("Edit", new { id = 17, });
        return Content(url);
    }

上のコードでは以下の操作が行われます。

  • /Gadget/Edit/17 が返されます。
  • UrlIUrlHelper を取得します。
  • Action が、アクション メソッドの絶対パスを使用して URL を生成します。 URL には、指定した action 名と route 値が含まれます。

次のコードでは、現在の要求と明示的な値 ({ page = "./Edit, id = 17, }) からアンビエント値を提供します。

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var url = Url.Page("./Edit", new { id = 17, });
        ViewData["URL"] = url;
    }
}

前述のコードでは、Edit Razor ページに次のページ ディレクティブが含まれている場合に、url/Edit/17 に設定します。

@page "{id:int}"

[編集] ページに "{id:int}" ルート テンプレートが含まれない場合は、url/Edit?id=17 になります。

MVC の IUrlHelper の動作により、ここで説明するルールに加えて、複雑なレイヤーが追加されます。

  • IUrlHelper は、常に現在の要求からルート値をアンビエント値として提供します。
  • IUrlHelper.Action は、開発者がオーバーライドしない場合を除き、常に現在の actioncontroller ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の page ルート値を明示的な値としてコピーします。
  • IUrlHelper.Page は、オーバーライドされない場合を除き、常に現在の handler ルート値を明示的な値として null にオーバーライドします。

ユーザーは、MVC が独自のルールに従っていないように見えるため、アンビエント値の動作の詳細にしばしば驚きます。 これまでの経緯および互換性の理由により、actioncontrollerpagehandler などの特定のルート値には、独自の特殊な動作があります。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage の同等の機能では、互換性のために IUrlHelper のこの異常と同じ動作が行われます。

URL の生成処理

一連の候補エンドポイントが見つかると、URL の生成アルゴリズムでは次が実行されます。

  • エンドポイントが反復処理されます。
  • 最初に成功した結果が返されます。

このプロセスの最初の手順はルート値の無効化と呼ばれます。 ルート値の無効化は、アンビエント値からどのルート値を使用する必要があり、無視する必要があるかをルーティングが決定するプロセスです。 各アンビエント値が検討され、明示的な値と組み合わされるか、または無視されます。

アンビエント値の役割について一番わかりやすい考え方は、一部の一般的なケースでアプリケーション開発者の入力作業が省かれるということです。 従来、アンビエント値が役に立つシナリオは MVC に関連しています。

  • 同じコントローラー内の別のアクションにリンクする場合、コントローラー名を指定する必要はありません。
  • 同じ領域内の別のコントローラーにリンクする場合、領域名を指定する必要はありません。
  • 同じアクション メソッドにリンクする場合は、ルート値を指定する必要はありません。
  • アプリの別の部分にリンクする場合は、アプリのその部分には意味のないルート値は引き継ぎません。

null を返す LinkGenerator または IUrlHelper の呼び出しは、通常、ルート値の無効化について理解していないことが原因で発生します。 ルート値の無効化のトラブルシューティングを行うには、さらにルート値を明示的に指定して、これにより問題が解決されるかどうかを確認します。

ルート値の無効化は、アプリの URL スキームが階層的であり、階層が左から右に形成されていることを前提として機能します。 基本的なコントローラー ルート テンプレート {controller}/{action}/{id?} について考えてみましょう。これが実際にどのように動作するかを直感的に理解できます。 値に対する変更により、右側に表示されるすべてのルート値が無効化されます。 これには、階層に関する前提が反映されています。 アプリに id のアンビエント値があり、操作によって controller に対して異なる値が指定された場合、

  • {controller}{id?} の左側にあるため、id は再利用されません。

この原則を示すいくつかの例を次に示します。

  • 明示的な値に id の値が含まれている場合、id のアンビエント値は無視されます。 controlleraction のアンビエント値を使用できます。
  • 明示的な値に action の値が含まれている場合、action のアンビエント値はすべて無視されます。 controller のアンビエント値を使用できます。 action の明示的な値が action のアンビエント値と異なる場合、id 値は使用されません。 action の明示的な値が action のアンビエント値と同じ場合、id 値を使用できます。
  • 明示的な値に controller の値が含まれている場合、controller のアンビエント値はすべて無視されます。 controller の明示的な値が controller のアンビエント値と異なる場合、actionid の値は使用されません。 controller の明示的な値が controller のアンビエント値と同じ場合、actionid の値を使用できます。

このプロセスは、属性ルートと専用規則ルートが存在することでさらに複雑になります。 {controller}/{action}/{id?} などのコントローラーの規則ルートでは、ルート パラメーターを使用して階層が指定されます。 コントローラーと Razor Pages に対する専用規則ルート属性ルートの場合、

  • ルート値の階層があります。
  • テンプレートには表示されません。

このような場合は、URL の生成によって必要な値の概念が定義されます。 コントローラーおよび Razor Pages によって作成されたエンドポイントには、ルート値の無効化を機能させるために必要な値が指定されています。

ルート値の無効化アルゴリズムの詳細は次のとおりです。

  • 必要な値の名前がルート パラメーターと組み合わされ、左から右に処理されます。
  • 各パラメーターについて、アンビエント値と明示的な値が比較されます。
    • アンビエント値と明示的な値が同じ場合、プロセスは続行されます。
    • アンビエント値が存在し、明示的な値が存在しない場合は、URL を生成するときにアンビエント値が使用されます。
    • アンビエント値が存在せず、明示的な値が存在する場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。
    • アンビエント値と明示的な値が存在し、2 つの値が異なる場合は、そのアンビエント値とそれ以降のすべてのアンビエント値が拒否されます。

この時点で、URL の生成操作はルート制約を評価する準備ができています。 許容可能な値のセットがパラメーターの既定値と組み合わされ、制約に提供されます。 すべての制約について合格した場合、操作が続行されます。

次に、許容可能な値を使用してルート テンプレートを展開できます。 ルート テンプレートは次のように処理されます。

  • 左から右。
  • 各パラメーターに、許容可能な値が代入されます。
  • 次の特殊なケースがあります。
    • 許容可能な値がなく、パラメーターに既定値がある場合は、既定値が使用されます。
    • 許容可能な値がなく、パラメーターが省略可能な場合は、処理が続行されます。
    • 存在しない省略可能なパラメーターの右側にあるルート パラメーターのいずれかに値がある場合、操作は失敗します。
    • 連続する既定値パラメーターと省略可能なパラメーターは、可能な場合、折りたたまれています。

ルートのセグメントと一致しない明示的に指定された値は、クエリ文字列に追加されます。 次の表は、ルート テンプレート {controller}/{action}/{id?} の使用時の結果をまとめたものです。

アンビエント値 明示的な値 結果
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

ルート値の無効化に関する問題

ASP.NET Core 3.0 の時点では、以前の ASP.NET Core バージョンで使用されている一部の URL 生成スキームが URL の生成においてうまく機能しません。 ASP.NET Core チームは、今後のリリースでこれらのニーズに対応する機能を追加することを計画しています。 当面は、従来のルーティングを使用することをお勧めします。

次のコードは、ルーティングでサポートされていない URL 生成スキームの例を示しています。

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("default", 
                                     "{culture}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute("blog", "{culture}/{**slug}", 
                                      new { controller = "Blog", action = "ReadPost", });
});

上記のコードでは、culture ルート パラメーターがローカライズに使用されています。 culture パラメーターを常にアンビエント値として許容されるようにすることが望まれます。 しかし、必要な値の動作方法が理由で、culture パラメーターはアンビエント値として許容されません。

  • "default" ルート テンプレートでは、culture ルート パラメーターは controller の左側にあるため、controller を変更しても culture は無効になりません。
  • "blog" ルート テンプレートでは、culture ルート パラメーターは controller の右側にあると見なされ、必要な値に表示されます。

エンドポイント メタデータの構成

次のリンクでは、エンドポイント メタデータの構成に関する情報を提供します。

RequireHost とルートが一致するホスト

RequireHost では、指定したホストが必要であるという制約がルートに適用されます。 RequireHost または [Host] パラメーターには、次のいずれかを指定できます。

  • ホスト: www.domain.com。任意のポートの www.domain.com と一致します。
  • ホストとワイルドカード: *.domain.com。任意のポートの www.domain.comsubdomain.domain.com、または www.subdomain.domain.com と一致します。
  • ポート: *:5000。任意のホストのポート 5000 と一致します。
  • ホストとポート: www.domain.com:5000 または *.domain.com:5000。ホストとポートと一致します。

RequireHost または [Host] を使用して、複数のパラメーターを指定できます。 制約は、いずれかのパラメーターに対して有効なホストと一致します。 たとえば、[Host("domain.com", "*.domain.com")]domain.comwww.domain.com、および subdomain.domain.com と一致します。

次のコードでは、RequireHost を使用して、指定したホストをルートに対して要求します。

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
            .RequireHost("contoso.com");
        endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
            .RequireHost("adventure-works.com");
        endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
    });
}

次のコードでは、コントローラー上の [Host] 属性を使用して、指定したホストのいずれかを要求します。

[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Host("example.com:8080")]
    public IActionResult Privacy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

[Host] 属性がコントローラーとアクション メソッドの両方に適用される場合は、次のようになります。

  • アクションの属性が使用されます。
  • コントローラーの属性は無視されます。

ルーティングに関するパフォーマンス ガイダンス

ASP.NET Core 3.0 では、ほとんどのルーティングが更新され、パフォーマンスが向上しました。

アプリにパフォーマンス上の問題がある場合、多くの場合ルーティングが問題として疑われます。 ルーティングが疑われる理由は、コントローラーや Razor Pages などのフレームワークにより、フレームワーク内で費やされた時間がログ メッセージで報告されるためです。 コントローラーによって報告された時間と要求の合計時間の間に大きな違いがある場合、次のようになります。

  • 開発者は、問題の発生源としてアプリ コードを排除します。
  • ルーティングが原因であると考えるのが一般的です。

ルーティングは、数千のエンドポイントを使用してパフォーマンス テストされています。 一般的なアプリでは、大きすぎるだけでパフォーマンスの問題が発生する可能性はほとんどありません。 ルーティングのパフォーマンス低下の最も一般的な根本原因は、通常、正しく動作していないカスタム ミドルウェアです。

次のコード サンプルは、遅延の原因を絞り込むための基本的な手法を示したものです。

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}

ルーティングの時間は次のように計ります。

  • 各ミドルウェアを、上記のコードに示されている時間を計るミドルウェアのコピーでインターリーブします。
  • 計られた時間データをコードと関連付けるための一意の識別子を追加します。

これは、遅延が 10ms を超えるなど顕著な場合に絞り込むための基本的な方法です。 Time 1 から Time 2 を引くことで、UseRouting ミドルウェア内で費やされた時間を報告します。

次のコードでは、前の時間を計るコードに対して、よりコンパクトなアプローチを使用します。

public sealed class MyStopwatch : IDisposable
{
    ILogger<Startup> _logger;
    string _message;
    Stopwatch _sw;

    public MyStopwatch(ILogger<Startup> logger, string message)
    {
        _logger = logger;
        _message = message;
        _sw = Stopwatch.StartNew();
    }

    private bool disposed = false;


    public void Dispose()
    {
        if (!disposed)
        {
            _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",
                                    _message, _sw.ElapsedMilliseconds);

            disposed = true;
        }
    }
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    int count = 0;
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }

    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}

潜在的にコストが高いルーティング機能

次の一覧に、基本的なルート テンプレートと比べて比較的コストが高いルーティング機能についての洞察を示します。

  • 正規表現: 複雑な正規表現を作成すること、つまり少量の入力で長い実行時間を実現することができます。
  • 複雑なセグメント ({x}-{y}-{z}):
    • 通常の URL パス セグメントを解析するよりもかなりコストがかかります。
    • より多くの部分文字列が割り当てられることになります。
    • ASP.NET Core 3.0 ルーティング パフォーマンスの更新では、複雑なセグメントのロジックは更新されませんでした。
  • 同期データ アクセス: 多くの複雑なアプリでは、ルーティングの一部としてデータベースにアクセスします。 ASP.NET Core 2.2 以前のルーティングでは、データベース アクセス ルーティングをサポートするための適切な拡張ポイントが提供されない場合があります。 たとえば、IRouteConstraintIActionConstraint は同期的です。 MatcherPolicyEndpointSelectorContext などの拡張ポイントは非同期的です。

ライブラリ作成者向けのガイダンス

このセクションでは、ルーティングを基盤とするライブラリ作成者向けのガイダンスを示します。 これらの詳細情報は、アプリの開発者が、ルーティングを拡張するライブラリとフレームワークを使用して優れたエクスペリエンスを実現できるようにすることを目的としています。

エンドポイントを定義する

URL 照合にルーティングを使用するフレームワークを作成するには、まず UseEndpoints の上に構築されるユーザー エクスペリエンスを定義します。

IEndpointRouteBuilder の上に構築します。 これにより、ユーザーは、他の ASP.NET Core 機能と混同せずにフレームワークを構成できます。 すべての ASP.NET Core テンプレートには、ルーティングが含まれます。 ルーティングが存在し、ユーザーになじみのあるものとします。

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...);

    endpoints.MapHealthChecks("/healthz");
});

IEndpointConventionBuilder を実装する MapMyFramework(...) の呼び出しから、シールドの具象型を返します。 ほとんどのフレームワーク Map... メソッドは、このパターンに従います。 IEndpointConventionBuilder インターフェイスは、

  • メタデータの構成可能性を許可します。
  • さまざまな拡張メソッドの対象とされています。

独自の型を宣言すると、独自のフレームワーク固有の機能をビルダーに追加できます。 フレームワークで宣言されたビルダーをラップし、呼び出しを転送するのは問題ありません。

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization()
                                 .WithMyFrameworkFeature(awesome: true);

    endpoints.MapHealthChecks("/healthz");
});

独自の EndpointDataSource を作成することを検討しますEndpointDataSource は、エンドポイントのコレクションを宣言および更新するための低レベルのプリミティブです。 EndpointDataSource は、コントローラーと Razor Pages によって使用される強力な API です。

ルーティング テストには、更新されていないデータ ソースの基本的な例があります。

既定では、EndpointDataSource の登録を試行しないでくださいUseEndpoints にフレームワークを登録するようユーザーに要求してください。 ルーティングの原理では、既定では何も含まれておらず、UseEndpoints がエンドポイントを登録する場所です。

ルーティング統合ミドルウェアを作成する

メタデータ型をインターフェイスとして定義することを検討します

クラスおよびメソッドの属性としてメタデータ型を使用できるようにします

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

コントローラーや Razor Pages などのフレームワークでは、型およびメソッドへのメタデータ属性の適用がサポートされています。 メタデータ型を宣言する場合:

  • 属性としてアクセスできるようにします。
  • ほとんどのユーザーが属性の適用に精通しています。

メタデータ型をインターフェイスとして宣言すると、柔軟性の高いレイヤーがさらに追加されます。

  • インターフェイスが構成可能です。
  • 開発者は、複数のポリシーを結合する独自の型を宣言できます。

次の例に示すように、メタデータをオーバーライドできるようにします

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

これらのガイドラインに従う最善の方法は、マーカーのメタデータを定義しないようにすることです。

  • メタデータ型が存在するかどうかを確認するだけで終わらせません。
  • メタデータのプロパティを定義し、プロパティを確認します。

メタデータ コレクションが順序付けされ、優先順位によるオーバーライドがサポートされます。 コントローラーの場合、アクション メソッドのメタデータが最も限定的です。

ルーティングを使用する場合もしない場合もミドルウェアが役立つようにします

app.UseRouting();

app.UseAuthorization(new AuthorizationPolicy() { ... });

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization();
});

このガイドラインの例として、UseAuthorization ミドルウェアを考えてみましょう。 この承認ミドルウェアを使用すると、フォールバック ポリシーを渡すことができます。 フォールバック ポリシーは、指定されている場合、次の両方に適用されます。

  • 指定されたポリシーのないエンドポイント。
  • エンドポイントに一致しない要求。

これにより、承認ミドルウェアはルーティングのコンテキストの外部で役に立ちます。 承認ミドルウェアは、従来のミドルウェア プログラミングに使用できます。

デバッグ診断

詳細なルーティング診断出力を行うには、Logging:LogLevel:MicrosoftDebug に設定してください。 開発環境では、appsettings.Development.json でログ レベルを次のように設定します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}