Start Debugging

ASP.NET Core 11 のエンドポイントフィルター vs ミドルウェア: どちらを使うべきか

ASP.NET Core 11 のための判断ガイド。ミドルウェアはハンドラーがバインドを行う前にすべてのリクエストで実行され、エンドポイントフィルターは一致したエンドポイントに対してのみ、バインドの後に実行され、型付き引数を見ることができます。比較表、それぞれを選ぶ場面、順序のルール、選択を強制する要点を含みます。

ロジックが、どのエンドポイントが一致するかに関わらず、あるいはそれより前にすべてのリクエストで実行される必要がある場合はミドルウェアを使います。例外処理、CORS、認証、レスポンス圧縮、静的ファイル、転送されたヘッダーなどです。ロジックがハンドラーのバインド済み引数を必要とする場合、または一部のエンドポイントだけに適用すべき場合はエンドポイントフィルターを使います。入力の検証、引数の正規化、エンドポイントごとの監査などです。最も鋭い判断基準はこうです。あなたのコードがハンドラーの受け取ろうとしている型付きモデルを必要とするなら、それはフィルターを求めています。フィルターはモデルのバインドの後に実行され、context.GetArgument<T>(index) を読めるからです。ルートが一致したかどうかに関わらず実行される必要があるなら、それはミドルウェアを求めています。ミドルウェアはルーティングがエンドポイントを解決する前に実行されるからです。以下はすべて、その判断の背後にある詳細です。この記事は .NET 11 (執筆時点では Preview 6、GA は 2026 年 11 月) を対象とし、Microsoft.NET.Sdk.Web と C# 14 を使いますが、どちらの機能も ASP.NET Core 7 以降で安定しているため、ここのすべての例は .NET 8、9、10 でも変更なく動作します。

比較表

これがあなたの求めてきた表です。上から下へ読めば、判断はたいてい自ずと定まります。

特性エンドポイントフィルターミドルウェア
実行される対象一致したエンドポイントのみそのパイプライン分岐上のすべてのリクエスト
ルーティングに対する位置ルーティングとモデルバインドの後ルーティングの前、最中、後 (配置による)
ハンドラー引数を見られるかはい、GetArgument<T>(index) で型付きにいいえ、生の HttpContext のみ
バインド済み引数を変更できるかはい、context.Arguments は変更可能いいえ、バインドはまだ行われていない
ショートサーキットの仕組みnext の代わりに IResult を返すnext(context) を呼ばない
スコープ制御エンドポイントごと、または MapGroup ごとアプリごと、または Map/UseWhen で分岐ごと
登録方法.AddEndpointFilter(...)app.Use(...) / app.UseMiddleware<T>()
戻り値の型ValueTask<object?>Task
エンドポイントが一致しないときの実行実行されないはい、エンドポイント実行より前に配置すれば
MVC コントローラーで再利用可能かはい、コントローラーのエンドポイントでもはい、パイプライン全体で

選択を実際に決めるのは最初の 3 行です。ミドルウェアはリクエストパイプラインの中に位置し、その区間を流れるすべてのリクエストがそれを実行します。どのエンドポイントも一致せず 404 になるリクエストでさえもです。エンドポイントフィルターは特定のルートハンドラーに結び付いており、そのハンドラーが選択されたときだけ実行されます。それは UseRouting がリクエストを照合し、フレームワークがルート値、クエリ文字列、リクエストボディをハンドラーのパラメーターにバインドした後に起こります。このタイミングの違いがすべてです。

ミドルウェアが見るもの、そしてそのタイミング

ミドルウェアはコンポーネントのチェーンであり、その各コンポーネントは HttpContextnext デリゲートを受け取ります。それらを Program.cs に順番に登録し、その順番こそが振る舞いです。リクエストは上から下へ流れ、レスポンスは下から上へと戻っていきます。

// .NET 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Runs for EVERY request, including ones that will 404.
    var sw = System.Diagnostics.Stopwatch.StartNew();
    await next(context);
    sw.Stop();
    app.Logger.LogInformation(
        "{Method} {Path} -> {Status} in {Elapsed}ms",
        context.Request.Method, context.Request.Path,
        context.Response.StatusCode, sw.ElapsedMilliseconds);
});

app.MapGet("/hello/{name}", (string name) => $"Hi {name}");

app.Run();

この計測用ミドルウェアは、ルーティングやあらゆる 404 を含め、リクエスト全体を計測します。アクセスできるのは文字列としての context.Request.Path だけです。name"world" にバインドされたことは見えません。外側のミドルウェアが実行される時点では、バインドがまだ行われていないからです。ミドルウェアはあなたのハンドラーの型システムより一段下で動作します。

UseRouting に対する位置は、多くの人が思うより重要です。現代の minimal hosting モデルでは、WebApplication がルーティングを自動的に挿入しますが、app.UseRouting() を明示的に呼び出して、分割が起こる場所を制御できます。ルーティングより前に登録されたミドルウェアは、エンドポイントが選択される前に実行されます。UseRouting より後に登録されたミドルウェアは、context.GetEndpoint() を通じて選択されたエンドポイントのメタデータを読めます。これが UseAuthorization がどのポリシーを適用すべきか知る仕組みです。だからこそ正統な順序は、UseRouting、次に UseAuthentication、次に UseAuthorization、そしてエンドポイント実行、なのです。認可はルーティングが生成したエンドポイントのメタデータを必要とします。

エンドポイントフィルターが見るもの、そしてそのタイミング

エンドポイントフィルターは単一のルートハンドラーの呼び出しをラップします。ルーティングの後、バインドの後に実行されるため、ミドルウェアが得られない唯一のものを持っています。あなたのハンドラーが受け取ろうとしている、実際の型付き引数です。

// .NET 11, C# 14
app.MapPost("/orders", (Order order) => Results.Created($"/orders/{order.Id}", order))
    .AddEndpointFilter(async (context, next) =>
    {
        // The Order is already bound. Middleware could never see this.
        var order = context.GetArgument<Order>(0);
        if (order.Quantity < 1)
        {
            return Results.Problem("Quantity must be at least 1.");
        }
        return await next(context);
    });

フィルターの戻り値の型は ValueTask<object?> です。任意の IResult (Results.Problem など) を返すとショートサーキットし、その結果がレスポンスに書き込まれ、ハンドラーは一度も呼ばれません。await next(context) を返すとハンドラーが実行され、その結果がチェーンを上へと戻されるため、フィルターは出ていく途中でレスポンスを変換することもできます。フィルターはバインド済みの Order を見られるので、検証は自然とここに置かれます。同じ仕事をしようとするミドルウェアコンポーネントは、リクエストボディを自分で読み直して再度デシリアライズしなければならず、フレームワークがすでに行った作業を重複させることになります。AddEndpointFilter の完全な仕組み、IEndpointFilter に基づくクラス形式、フィルターの順序については、minimal API にエンドポイントフィルターを追加する方法 で扱っています。この記事は、そもそもいつミドルウェアより先にそれを選ぶべきか、についてのものです。

いつミドルウェアを選ぶか

いつエンドポイントフィルターを選ぶか

パフォーマンスが実際に判断に入り込む唯一の場所

「ミドルウェアはすべてに対して実行されるので無駄だから」という理由でフィルターに手を伸ばしたくなります。それをスループットの競争として枠付けするのは我慢してください。どちらの機能も薄いものです。フィルターは ValueTask<object?> を返すデリゲートであり、ミドルウェアコンポーネントは Task を返すデリゲートで、どちらの呼び出しごとのオーバーヘッドも、データベースに触れたり JSON をシリアライズしたりする実際のハンドラーの隣では無視できます。意味のある違いは呼び出しごとのコストではなく、何回呼び出されるかです。パイプラインの早い位置に置かれたミドルウェアコンポーネントはすべてのリクエストで実行されるので、そこでの高価な作業 (データベースクエリ、大きなメモリ割り当て) は、あらゆる 404 とあらゆる health-check の ping によって支払われます。エンドポイントフィルターにある同じ作業は、そのエンドポイントが選択されたときだけ実行されます。だからパフォーマンスのルールは「フィルターの方が速い」ではなく「作業を必要な場所に限定せよ」です。横断的関心事が本当にすべてのルートに適用されるなら、ミドルウェアはそれの正しい、そして遅くもない置き場所です。ひとにぎりのエンドポイントに適用されるなら、フィルターはそれらのエンドポイントに決して触れない数千のリクエストでそれを実行するのを避けます。それはパフォーマンスの判断に見せかけたスコープの判断であり、その主張の正直な形です。

あなたの代わりに選んでくれる要点

いくつかの硬い制約が、好みを完全に上書きします。

フィルターはルーティングより前に実行できません、決して。 要件が「ルーターが見る前にリクエストを拒否する」または「URL を書き換える」なら、フィルターは物理的にそれができません。フィルターはルーティングの下流にあるエンドポイント実行の中に存在するからです。これはミドルウェアを強制します。

ミドルウェアは作業をやり直さずにはバインド済みモデルを見られません。 要件が「デシリアライズされたリクエストボディを検証する」なら、ミドルウェアはボディを自分でバッファリングしてデシリアライズしなければならず、その後フレームワークがハンドラーのためにそれを再びデシリアライズします。この二重のバインドは、あなたがフィルターを求めていた強いサインです。これはフィルターを強制します。

例外はフィルターのスコープから逃れます。 フィルターは自分のエンドポイントだけをラップするので、アプリ全体の安全網にはなれません。唯一の例外処理をフィルターに置くと、別のミドルウェアやルーティング中に投げられた例外はそれを素通りし、デフォルトの 500 ハンドラーに当たります。グローバルなエラー処理はミドルウェアを強制します。

順序のモデルが異なり、それらを混ぜると人を混乱させます。 ミドルウェアは Program.cs での登録順にネストします。フィルターは .AddEndpointFilter の呼び出しを連結した順にネストします。最初に登録されたものが next の前のコードを最初に、next の後のコードを最後に実行します。両方を積み重ねると、あるエンドポイントのフィルターチェーン全体が、UseRoutingUseAuthenticationUseAuthorization が実行された後の、ミドルウェアパイプラインの最も内側の点の中で実行されます。したがって認可は常に、どのエンドポイントフィルターよりも前に実行されます。これはたいてい望ましいことですが、フィルターは認証スキームを実装する場所としては誤りだということを意味します。認証はミドルウェアを強制します。

終端の振る舞いは逆です。 next を呼ばないミドルウェアコンポーネントは、単に続行しないことでショートサーキットします。フィルターは IResult を返すことでショートサーキットします。フィルターを書いてショートサーキットのパスで何かを返し忘れると、静かに飲み込まれたリクエストではなく、コンパイルエラーか null 結果が得られます。これはフィルターにとって小さいながら実在する使い勝手の利点です。

推奨、あらためて

デフォルトはこうです。すべてのリクエストで、またはルーティングより前に実行される必要がある横断的関心事はミドルウェアです。ハンドラーの型付き引数を必要とする関心事、またはエンドポイントの部分集合に適用される関心事はエンドポイントフィルターです。認証、CORS、例外処理、圧縮、静的ファイルはミドルウェアであり、これからも常にそうです。検証、引数の正規化、エンドポイントごとの監査、結果の整形はエンドポイントフィルターです。グレーゾーンの場合はエンドポイントごとの認可ロジックです。HttpContext.User の claims だけを必要とするならどちらでも動きますが、ポリシーがそれの守るエンドポイントの隣に存在するようフィルターを選びましょう。判断を下すのにバインド済み引数を必要とするなら (バインド済みのエンティティ id に対する行レベルのアクセスチェックなど)、それはフィルターでなければなりません。本当に決められないときは、ほとんどすべての場合を解決する唯一の問いを立ててください。このコードは、私のハンドラーが受け取る引数を見る必要があるか。はいならフィルターです。いいえで、ルートに関わらず実行される必要があるなら、ミドルウェアです。

関連記事

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る