0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ASP.NET Core】Middleware と Filters による共通処理の実装

0
Posted at

【ASP.NET Core】Middleware と Filters による共通処理の実装

HTTPパイプライン / Actionフック / Program.cs登録 / React連携まで整理


ASP.NET Core の Web アプリケーションでは、リクエストは HTTPパイプライン を通って Controller に到達します。

このパイプラインの中で認証・ロギング・例外処理・バリデーション・共通レスポンスなどの 横断的処理 を Controller に書いてしまうと、Controller が肥大化してロジックが分散し、保守性が下がるという問題が発生します。

そこで ASP.NET Core には MiddlewareFilters という仕組みが用意されています。

この記事では以下をまとめます。

  • 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 アプリ を作れるようになります。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?