【ASP.NET Core】Middleware と Filters による共通処理の実装
HTTPパイプライン / Actionフック / Program.cs登録 / React連携まで整理
ASP.NET Core の Web アプリケーションでは、リクエストは HTTPパイプライン を通って Controller に到達します。
このパイプラインの中で認証・ロギング・例外処理・バリデーション・共通レスポンスなどの 横断的処理 を Controller に書いてしまうと、Controller が肥大化してロジックが分散し、保守性が下がるという問題が発生します。
そこで ASP.NET Core には Middleware と Filters という仕組みが用意されています。
この記事では以下をまとめます。
- Middleware の仕組みと具体的な使い方
- Filters の種類と具体的な使い方
- Program.cs での登録方法
- Middleware と Filter の使い分け
- 両者を組み合わせてできること
- React と組み合わせると何ができるか
ASP.NET Core HTTP パイプライン
ASP.NET Core ではリクエストは次の流れで処理されます。
ポイント
- Middleware は パイプライン全体 に適用される
- Filter は Controller / Action 実行時 にのみ動作する
- Middleware は任意の数を連鎖させることができる
Middleware とは
Middleware は HTTPリクエストパイプライン全体を制御するコンポーネント です。
特徴
- 全リクエストに適用される
- リクエスト前後の両方で処理できる
- 次の Middleware を呼ばずに処理を終了できる(ショートサーキット)
- Controller の情報には直接アクセスできない
Middleware の具体的な使い方
例1: リクエストログを出す Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// リクエスト前
_logger.LogInformation("Request: {method} {path}",
context.Request.Method,
context.Request.Path);
await _next(context);
// レスポンス後
_logger.LogInformation("Response: {statusCode}",
context.Response.StatusCode);
}
}
使いどころ
- 全 API のアクセスログ
- パフォーマンス計測
- 監査ログ
例2: APIキーを確認する Middleware
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly string _validApiKey;
private const string ApiKeyHeaderName = "X-API-KEY";
public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
// APIキーは設定ファイルや環境変数から取得する
_validApiKey = configuration["ApiKey"]
?? throw new InvalidOperationException("ApiKey is not configured.");
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/api"))
{
if (!context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("API Key is missing.");
return;
}
// ヘッダーの存在確認だけでなく、値の検証も必ず行う
if (!string.Equals(extractedApiKey, _validApiKey, StringComparison.Ordinal))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Invalid API Key.");
return;
}
}
await _next(context);
}
}
注意: APIキーはコードにハードコードせず、
appsettings.jsonや環境変数から取得してください。値の検証を省略すると、ヘッダーが存在するだけで通過してしまいます。
使いどころ
- 特定 API の事前認証
- 管理 API の入口制御
- 外部連携用の簡易保護
Program.cs での Middleware 登録方法
Middleware は 登録順が実行順 です。
var app = builder.Build();
// 1. 例外処理は最初に登録する(全Middlewareのエラーを捕捉できるようにするため)
app.UseExceptionHandler("/Error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// 2. カスタム Middleware(認証前のログも取りたい場合は UseAuthentication より前に置く)
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<ApiKeyMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
登録順の考え方
| 順番 | 内容 |
|---|---|
| 1 | 例外処理(UseExceptionHandler / UseDeveloperExceptionPage) |
| 2 | HTTPS / 静的ファイル |
| 3 | Routing |
| 4 | ログ / カスタム共通処理 |
| 5 | Authentication |
| 6 | Authorization |
| 7 | MVC / API 実行 |
例外処理の Middleware はパイプラインの先頭に配置します。後ろに置くと、それより前の Middleware で発生した例外を捕捉できなくなります。
Filter とは
Filter は Controller / Action 実行の前後に介入する仕組み です。
Middleware との主な違い
- Controller の情報にアクセスできる
- Action 引数を参照できる
- ModelState を参照できる
- Action 単位で適用範囲を絞れる
Filter の種類
| Filter | 用途 |
|---|---|
| AuthorizationFilter | 認可処理(最初に実行される) |
| ResourceFilter | キャッシュ制御など Action 前後の軽量処理 |
| ActionFilter | Action 実行前後 |
| ExceptionFilter | 例外処理 |
| ResultFilter | ActionResult 前後 |
| PageFilter | Razor Pages 向けの Action 相当処理 |
Filter の具体的な使い方
例1: Action 実行時間を計測する ActionFilter
IActionFilter をスレッドセーフに実装するため、計測値は HttpContext.Items に持たせます。
using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;
public class ExecutionTimeFilter : IActionFilter
{
private readonly ILogger<ExecutionTimeFilter> _logger;
public ExecutionTimeFilter(ILogger<ExecutionTimeFilter> logger)
{
_logger = logger;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// インスタンスフィールドではなく HttpContext.Items に格納してスレッドセーフにする
context.HttpContext.Items["Stopwatch"] = Stopwatch.StartNew();
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Items["Stopwatch"] is Stopwatch sw)
{
sw.Stop();
_logger.LogInformation("Action {actionName} executed in {elapsed} ms",
context.ActionDescriptor.DisplayName,
sw.ElapsedMilliseconds);
}
}
}
注意:
Stopwatchをインスタンスフィールドに持つと、Filter がシングルトンやスコープで登録された場合にスレッドセーフでなくなります。HttpContext.Itemsを使うとリクエストごとに安全に管理できます。
使いどころ
- 特定画面の性能計測
- API レスポンスの遅い箇所特定
- 重い Action の監視
例2: モデルエラー時に共通レスポンスを返す ActionFilter
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class ValidateModelFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new
{
success = false,
errors = context.ModelState
});
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
使いどころ
- API 入力チェックの共通化
- Controller から
if (!ModelState.IsValid)を減らす - React 向け API のエラー形式統一
例3: 例外をステータスコード別に JSON で返す ExceptionFilter
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class CustomExceptionFilter : IExceptionFilter
{
private readonly ILogger<CustomExceptionFilter> _logger;
public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Unhandled exception occurred.");
// 例外の種類に応じてステータスコードを変える
// クライアント起因のエラーは 400 系、それ以外は 500 系にする
var (statusCode, message) = context.Exception switch
{
ArgumentException => (StatusCodes.Status400BadRequest, "リクエストが不正です。"),
UnauthorizedAccessException => (StatusCodes.Status403Forbidden, "アクセス権がありません。"),
KeyNotFoundException => (StatusCodes.Status404NotFound, "対象のリソースが見つかりません。"),
_ => (StatusCodes.Status500InternalServerError, "サーバー内部でエラーが発生しました。")
};
context.Result = new ObjectResult(new
{
success = false,
message
})
{
StatusCode = statusCode
};
context.ExceptionHandled = true;
}
}
注意: 全例外を
BadRequest (400)で返すと、クライアント側がサーバー起因のエラーとクライアント起因のエラーを区別できなくなります。例外の種類に応じて適切なステータスコードを返してください。
使いどころ
- API エラーレスポンスを統一
- フロントエンドへのエラー形式固定
- 開発中の原因追跡
Filter の登録方法
方法1: 特定 Controller / Action に適用
[ServiceFilter(typeof(ExecutionTimeFilter))]
public class ProductsController : Controller
{
public IActionResult Index()
{
return View();
}
}
方法2: Program.cs でグローバル登録
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add<CustomExceptionFilter>();
options.Filters.Add<ValidateModelFilter>();
});
DI への登録
コンストラクタで依存を受け取る Filter は DI に登録が必要です。
builder.Services.AddScoped<ExecutionTimeFilter>();
builder.Services.AddScoped<CustomExceptionFilter>();
builder.Services.AddScoped<ValidateModelFilter>();
AddScopedはリクエストごとに新しいインスタンスを生成します。グローバル登録でもスレッドセーフに動作させるために、原則としてAddScopedを使います。[ServiceFilter(typeof(T))]で適用する場合も DI 登録が必要です。
Middleware と Filter の違い
| 特徴 | Middleware | Filter |
|---|---|---|
| 適用範囲 | アプリ全体 | Controller / Action |
| 実行タイミング | HTTP パイプライン | MVC 実行時 |
| アクセス情報 | HttpContext | Controller 情報 / ModelState / Action 引数 |
| ショートサーキット | 可能 | 可能 |
| 主な用途 | 認証 / ログ / 静的ファイル / CORS | バリデーション / 特定 Action 制御 / 例外整形 |
実務での使い分け
基本ルール
Web 全体に関わる処理 → Middleware
Controller / Action に依存する処理 → Filter
Middleware 向き
- ログ出力
- 認証前の共通処理
- CORS
- HTTPS 強制
- レート制限
- API キー検証
- 全体例外処理
Filter 向き
- ModelState チェック
- Action 実行時間計測
- 特定 API の権限チェック
- 例外レスポンスの整形
- Action ごとの前後処理
Middleware と Filter を組み合わせると何ができるか
実務では片方だけで完結するより、組み合わせた方が強力 です。
例1: ログ + バリデーション + 例外整形
| 役割 | 実装箇所 |
|---|---|
| 全リクエストログ取得 | Middleware |
| 入力チェック | ActionFilter |
| API エラー JSON 統一 | ExceptionFilter |
これにより、どのリクエストが来たか追跡でき、不正入力を共通で弾き、フロント側は同じ形式のエラーだけを扱えばよくなります。
例2: API キー認証 + 実行時間計測
| 役割 | 実装箇所 |
|---|---|
| API キーチェック | Middleware |
| Action ごとの実行時間記録 | ActionFilter |
認証前に弾いて、通過した処理だけ性能分析できます。
例3: 監査ログ + 結果整形
| 役割 | 実装箇所 |
|---|---|
| 誰がどの URL へ来たか記録 | Middleware |
| レスポンス前に結果を加工 | ResultFilter |
全体トレースと特定 API のレスポンス形式統一を両立できます。
React と組み合わせると何ができるか
ASP.NET Core API を React から利用する場合、Middleware と Filter の価値はさらに高くなります。
1. CORS を Middleware で設定する
React(別オリジン)から API を呼ぶ場合、CORS の設定が必要です。
// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("ReactApp", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Middleware として登録(UseRouting より前に置く)
app.UseCors("ReactApp");
2. React 向け API のエラー形式を統一する
React 側は API エラーの形式が統一されていると扱いやすくなります。
ASP.NET Core 側(ExceptionFilter)
public class ApiExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var statusCode = context.Exception switch
{
ArgumentException => StatusCodes.Status400BadRequest,
UnauthorizedAccessException => StatusCodes.Status403Forbidden,
KeyNotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
context.Result = new ObjectResult(new
{
success = false,
message = context.Exception.Message
})
{
StatusCode = statusCode
};
context.ExceptionHandled = true;
}
}
React 側
const fetchProduct = async (id) => {
try {
const res = await fetch(`/api/products/${id}`);
const data = await res.json();
if (!res.ok) {
throw new Error(data.message);
}
return data;
} catch (err) {
console.error("API Error:", err.message);
}
};
3. React からの入力エラーを共通で返す
Filter で ModelState をチェックし、React に統一形式の JSON を返すようにします。
ASP.NET Core 側
public class ValidateModelForApiFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new
{
success = false,
errors = context.ModelState
});
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
React 側
const createUser = async (formData) => {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
const data = await res.json();
if (!res.ok) {
// errors にフォームフィールドごとのエラーが入る
console.log(data.errors);
}
};
4. React の画面操作をログとして残す
Middleware で /api/... へのアクセスを記録すると、どの画面からどの API が何回呼ばれたかを追跡できます。これは操作ログ・不正アクセス検知・UX 分析に役立ちます。
5. 認証・認可を React API と連携しやすくする
Middleware で認証処理、Filter で API 単位の権限チェックを行うことで、管理画面・一般ユーザー画面・管理者専用 API を整理しやすくなります。
実務アーキテクチャ例
ベストプラクティス
Controller はビジネスロジックに集中させる
NG: Controller に横断的処理が混在している
public class OrdersController : Controller
{
public IActionResult Create(OrderRequest request)
{
// ログ
_logger.LogInformation("Order creation started.");
// 認証チェック
if (!User.IsInRole("Admin")) return Forbid();
// バリデーション
if (!ModelState.IsValid) return BadRequest(ModelState);
// 例外処理
try
{
_orderService.Create(request);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
return Ok();
}
}
OK: 横断的処理を Middleware / Filter に分離する
// Middleware → ログ / CORS / 認証
// Filter → バリデーション / 例外整形
// Controller → ビジネスロジックのみ
public class OrdersController : Controller
{
public IActionResult Create(OrderRequest request)
{
_orderService.Create(request);
return Ok();
}
}
役割の整理まとめ
| レイヤー | 担当 |
|---|---|
| Middleware | HTTP パイプライン全体 / ログ / CORS / レート制限 / API キー検証 |
| Filter | Controller / Action 単位 / ModelState / 例外整形 / 実行前後処理 |
| Controller | ビジネスロジックのみ |
Middleware と Filter を適切に設計することで、Controller をシンプルに保ちながら、堅牢で拡張しやすい Web アプリ を作れるようになります。