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 がリクエストを照合し、フレームワークがルート値、クエリ文字列、リクエストボディをハンドラーのパラメーターにバインドした後に起こります。このタイミングの違いがすべてです。
ミドルウェアが見るもの、そしてそのタイミング
ミドルウェアはコンポーネントのチェーンであり、その各コンポーネントは HttpContext と next デリゲートを受け取ります。それらを 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 にエンドポイントフィルターを追加する方法 で扱っています。この記事は、そもそもいつミドルウェアより先にそれを選ぶべきか、についてのものです。
いつミドルウェアを選ぶか
- その関心事がグローバルでルートに依存しないとき。 例外処理 (
UseExceptionHandler)、HTTPS リダイレクト、HSTS、CORS、レスポンス圧縮、静的ファイル、転送ヘッダーの処理は、どのエンドポイント (あるとして) が一致するかに関わらず、すべてのリクエストで実行される必要があります。フィルターは「すべてに対して実行する」を表現できません。フィルターはエンドポイントに結び付いており、404 にはエンドポイントがないからです。特にレスポンス圧縮はパイプラインに属します。これは ASP.NET Core 11 API にレスポンス圧縮を追加する で扱っています。 - ルーティングより前に実行する必要があるとき。 パスの書き換え、プレフィックスの除去、ルーターが見る前のリクエストの拒否は、本質的にミドルウェアの仕事です。エンドポイントフィルターはルートが一致した後に実行されるため、ルーティングに影響を与えるには遅すぎます。
- アプリ全体で例外を捕捉しているとき。
UseExceptionHandlerと開発者向け例外ページは、下流のパイプライン全体をラップします。フィルターはただ一つのエンドポイントだけをラップするので、ルーティング中や別のミドルウェアで投げられた例外は決してそこに届きません。グローバルなエラー処理はパイプラインの関心事であり、だからこそ グローバル例外フィルターの設定 もエンドポイントごとではなくアプリレベルで登録されます。 - ロジックが 404 になるリクエストを見る必要があるとき。 メトリクス、リクエストのログ記録、レート制限は、エンドポイントに決して一致しないリクエストを数えたり絞ったりする必要がしばしばあります。ミドルウェアはそれらを見ますが、フィルターは見ません。
いつエンドポイントフィルターを選ぶか
- バインド済み引数が必要なとき。
Productの検証、pageクエリパラメーターが範囲内にあるかの確認、文字列の正規化は、すべて型付きの値を必要とします。context.GetArgument<T>(index)と変更可能なリストcontext.Argumentsがまさにそれを与えてくれ、ミドルウェアには等価物がありません。 - その関心事が全部ではなく一部のエンドポイントに適用されるとき。 フィルターは単一のエンドポイントに、または
MapGroupを通じてそのグループにアタッチされます。検証がPOST /productsとPUT /products/{id}にだけ意味を持つなら、グループフィルターがグローバルパイプラインを汚さずにそれを正確に限定します。これは MapGroup で minimal API のエンドポイントを整理する で説明されているリソースごとのモジュールと組み合わさります。 - ハンドラーの結果を検査または書き換えたいとき。 フィルターの戻り値はチェーンを戻っていくので、成功した結果をエンベロープで包んだり、キャッシュのヒントを追加したり、ドメインの結果を
IResultに変換したりできます。ミドルウェアは生のレスポンスストリームしか操作できず、ハンドラーが書き込みを始めた後ではずっと不器用になります。 - minimal API とコントローラーで同じロジックを使いたいとき。
AddEndpointFilterはコントローラーのエンドポイント規約ビルダーでも動作するので、単一のフィルターデリゲートが、ルートを共有する minimal エンドポイントと MVC アクションの両方を守れます。
パフォーマンスが実際に判断に入り込む唯一の場所
「ミドルウェアはすべてに対して実行されるので無駄だから」という理由でフィルターに手を伸ばしたくなります。それをスループットの競争として枠付けするのは我慢してください。どちらの機能も薄いものです。フィルターは ValueTask<object?> を返すデリゲートであり、ミドルウェアコンポーネントは Task を返すデリゲートで、どちらの呼び出しごとのオーバーヘッドも、データベースに触れたり JSON をシリアライズしたりする実際のハンドラーの隣では無視できます。意味のある違いは呼び出しごとのコストではなく、何回呼び出されるかです。パイプラインの早い位置に置かれたミドルウェアコンポーネントはすべてのリクエストで実行されるので、そこでの高価な作業 (データベースクエリ、大きなメモリ割り当て) は、あらゆる 404 とあらゆる health-check の ping によって支払われます。エンドポイントフィルターにある同じ作業は、そのエンドポイントが選択されたときだけ実行されます。だからパフォーマンスのルールは「フィルターの方が速い」ではなく「作業を必要な場所に限定せよ」です。横断的関心事が本当にすべてのルートに適用されるなら、ミドルウェアはそれの正しい、そして遅くもない置き場所です。ひとにぎりのエンドポイントに適用されるなら、フィルターはそれらのエンドポイントに決して触れない数千のリクエストでそれを実行するのを避けます。それはパフォーマンスの判断に見せかけたスコープの判断であり、その主張の正直な形です。
あなたの代わりに選んでくれる要点
いくつかの硬い制約が、好みを完全に上書きします。
フィルターはルーティングより前に実行できません、決して。 要件が「ルーターが見る前にリクエストを拒否する」または「URL を書き換える」なら、フィルターは物理的にそれができません。フィルターはルーティングの下流にあるエンドポイント実行の中に存在するからです。これはミドルウェアを強制します。
ミドルウェアは作業をやり直さずにはバインド済みモデルを見られません。 要件が「デシリアライズされたリクエストボディを検証する」なら、ミドルウェアはボディを自分でバッファリングしてデシリアライズしなければならず、その後フレームワークがハンドラーのためにそれを再びデシリアライズします。この二重のバインドは、あなたがフィルターを求めていた強いサインです。これはフィルターを強制します。
例外はフィルターのスコープから逃れます。 フィルターは自分のエンドポイントだけをラップするので、アプリ全体の安全網にはなれません。唯一の例外処理をフィルターに置くと、別のミドルウェアやルーティング中に投げられた例外はそれを素通りし、デフォルトの 500 ハンドラーに当たります。グローバルなエラー処理はミドルウェアを強制します。
順序のモデルが異なり、それらを混ぜると人を混乱させます。 ミドルウェアは Program.cs での登録順にネストします。フィルターは .AddEndpointFilter の呼び出しを連結した順にネストします。最初に登録されたものが next の前のコードを最初に、next の後のコードを最後に実行します。両方を積み重ねると、あるエンドポイントのフィルターチェーン全体が、UseRouting、UseAuthentication、UseAuthorization が実行された後の、ミドルウェアパイプラインの最も内側の点の中で実行されます。したがって認可は常に、どのエンドポイントフィルターよりも前に実行されます。これはたいてい望ましいことですが、フィルターは認証スキームを実装する場所としては誤りだということを意味します。認証はミドルウェアを強制します。
終端の振る舞いは逆です。 next を呼ばないミドルウェアコンポーネントは、単に続行しないことでショートサーキットします。フィルターは IResult を返すことでショートサーキットします。フィルターを書いてショートサーキットのパスで何かを返し忘れると、静かに飲み込まれたリクエストではなく、コンパイルエラーか null 結果が得られます。これはフィルターにとって小さいながら実在する使い勝手の利点です。
推奨、あらためて
デフォルトはこうです。すべてのリクエストで、またはルーティングより前に実行される必要がある横断的関心事はミドルウェアです。ハンドラーの型付き引数を必要とする関心事、またはエンドポイントの部分集合に適用される関心事はエンドポイントフィルターです。認証、CORS、例外処理、圧縮、静的ファイルはミドルウェアであり、これからも常にそうです。検証、引数の正規化、エンドポイントごとの監査、結果の整形はエンドポイントフィルターです。グレーゾーンの場合はエンドポイントごとの認可ロジックです。HttpContext.User の claims だけを必要とするならどちらでも動きますが、ポリシーがそれの守るエンドポイントの隣に存在するようフィルターを選びましょう。判断を下すのにバインド済み引数を必要とするなら (バインド済みのエンティティ id に対する行レベルのアクセスチェックなど)、それはフィルターでなければなりません。本当に決められないときは、ほとんどすべての場合を解決する唯一の問いを立ててください。このコードは、私のハンドラーが受け取る引数を見る必要があるか。はいならフィルターです。いいえで、ルートに関わらず実行される必要があるなら、ミドルウェアです。
関連記事
- ASP.NET Core 11 で minimal API にエンドポイントフィルターを追加する方法
- ASP.NET Core 11 で MapGroup を使って minimal API のエンドポイントを整理する方法
- ASP.NET Core 11 でグローバル例外フィルターを追加する方法
- ASP.NET Core 11 API にレスポンス圧縮を追加する方法
- ASP.NET Core 11 の minimal API vs コントローラー
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.