動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

ASP.NET MVC のアクションフィルターで認証・例外・ログを横断的に挟む

バイブス父さん
現役の業務SE
2026年9月13日13 min read広告 (PR) を含む場合があります
ASP.NET MVC のアクションフィルターで認証・例外・ログを横断的に挟む

みなさんこんにちは!ヒロポンです!!

コントローラのアクションを開くたびに、こんな行が並んでませんか??

if (Session["UserId"] == null) return RedirectToAction("Login");
_logger.Info("Index 開始");
var sw = Stopwatch.StartNew();

ログイン確認、ログ出力、実行時間の計測。

どのアクションにも同じ前処理・後処理をコピペしてる。1個直すたびに全アクションを直して回る、あれです。

これ、ASP.NET MVC の アクションフィルター に寄せると、コピペが1箇所に集約できます。

今日は ASP.NET のアクションフィルターの使い方を、4種類の役割分担と実行順序から積み上げて解説します。「認証→アクション→結果」の順序を誤解すると例外処理が効かない、という詰まりどころまで。

アクションフィルターは何を解決するのか

「ログイン確認」「例外ログ」「実行時間の計測」——これらは、どの機能にも共通で必要だけど、その機能の本質ではない 処理です。

こういうのを 横断的関心事(cross-cutting concern) と呼びます。

本質じゃないのに、アクションごとに毎回書く。しかも書き忘れるとログイン確認が抜けてセキュリティホールになる。

アクションフィルターは、この横断的な処理を アクションの前後に自動で挟み込む 仕組みです。こんな感じで、本質じゃない処理を外に追い出せる。コントローラ本体は業務ロジックだけに集中できます。

ここまでのポイント: フィルター=「どのアクションにも共通する前後処理」を1箇所にまとめる仕組み。

フィルターが無い時代の書き方

フィルターを使わないと、こうなります。

public ActionResult Index()
{
    if (Session["UserId"] == null) return RedirectToAction("Login");
    var sw = Stopwatch.StartNew();
    try
    {
        // ここが本当にやりたい処理
        var data = _service.GetData();
        return View(data);
    }
    catch (Exception ex)
    {
        _logger.Error(ex);
        return View("Error");
    }
    finally
    {
        sw.Stop();
        _logger.Info($"Index: {sw.ElapsedMilliseconds}ms");
    }
}

本体2行のために、周りが10行。

これがアクションの数だけ増殖する。フィルターは、この「周り」を全部引き剥がすためのものです。

4種類のフィルターと実行順序

ASP.NET MVC のフィルターは、役割で4種類に分かれています。

ASP.NET MVC の4種類のフィルターの役割とインターフェースの比較表

大事なのは 実行順序 です。ここを絵にすると、こうなります。

リクエストから認証・アクション・結果・例外までのフィルター実行順序のフロー図

認証 → アクション → 結果 の順で流れて、途中で例外が出たら例外フィルターに落ちる。この順番が、後でハマりポイントになります。

ここまでのポイント: フィルターは4種類。実行順序は「認証→アクション→結果」、例外は横から拾う。

実装: ログと実行時間をアクションフィルターで挟む

まず一番使うアクションフィルター。ActionFilterAttribute を継承して、前後のメソッドをオーバーライドします。

using System.Diagnostics;
using System.Web.Mvc;

public class LogActionFilter : ActionFilterAttribute
{
    private const string Key = "__sw";

    // アクションの直前に呼ばれる
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.HttpContext.Items[Key] = Stopwatch.StartNew();
        var name = filterContext.ActionDescriptor.ActionName;
        Trace.WriteLine($"[開始] {name}");
    }

    // アクションの直後に呼ばれる
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var sw = (Stopwatch)filterContext.HttpContext.Items[Key];
        sw.Stop();
        var name = filterContext.ActionDescriptor.ActionName;
        Trace.WriteLine($"[終了] {name}: {sw.ElapsedMilliseconds}ms");
    }
}

OnActionExecuting がアクションの前、OnActionExecuted が後。

これをコントローラかアクションに属性で付けるだけで、計測ロジックが本体から消えます。

[LogActionFilter]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        var data = _service.GetData();
        return View(data);   // 計測もログも、もう書かなくていい
    }
}

いい感じにコントローラが痩せましたよね。

例外フィルターと認証フィルター

例外ログは、IExceptionFilter に寄せます。

using System.Web.Mvc;

public class LogExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        if (filterContext.ExceptionHandled) return;

        Trace.WriteLine($"[例外] {filterContext.Exception}");

        filterContext.Result = new ViewResult { ViewName = "Error" };
        filterContext.ExceptionHandled = true;   // ★ これを忘れると再スローされる
    }
}

認証は、標準の AuthorizeAttribute を継承してチェックを差し込みます。

using System.Web;
using System.Web.Mvc;

public class ApiKeyFilter : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        return httpContext.Request.Headers["X-Api-Key"] == "expected-key";
    }
}

全アクションに効かせたいものは、FilterConfig でグローバル登録する。

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new LogExceptionFilter());   // 例外ログは全体で
        filters.Add(new LogActionFilter());      // 計測も全体で
    }
}

ここまでのポイント: 例外は IExceptionFilter、認証は AuthorizeAttribute、全体に効かせるならグローバル登録。

実行順序を誤解すると例外処理が効かない

ここが一番の詰まりどころです。フィルターを覚えたての頃に、俺も同じ勘違いをして半日つぶしました。

誤解1: 「例外フィルターを付ければ、どこの例外も全部拾える」

例外フィルターが拾うのは、基本的に アクション実行中 に投げられた例外です。実行順序が「認証→アクション→結果」なので、こうなります。

  • 認証フィルターより前(ルーティングなど)の例外は拾わない
  • 結果(View のレンダリング)中の例外は、状況によっては差し替えが間に合わない

「なんで例外フィルターで拾えないん??」の8割は、例外が出た場所が想定とズレてるパターン。順序の図(認証→アクション→結果)を思い出すと、拾える範囲が見えてきます。

誤解2: OnActionExecuted で「拾ったつもり」で再スローされる

OnActionExecuted には filterContext.Exception があって、アクションで出た例外を見られます。ここでログを吐いて「対処した」つもりでも——

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
    if (filterContext.Exception != null)
    {
        Trace.WriteLine(filterContext.Exception.Message);
        // ★ ExceptionHandled = true を立てないと、この後で例外が再スローされる
        filterContext.ExceptionHandled = true;
    }
}

filterContext.ExceptionHandled = true を立て忘れると、例外はそのまま上流に流れて、結局エラー画面(YSOD)に落ちる。

「ログは出てるのにエラー画面が出る」ときは、だいたいこれです。

ここまでのポイント: 例外フィルターが拾うのは主にアクション中の例外。握りつぶすなら ExceptionHandled = true を忘れない。

属性で個別に付ける vs グローバル登録

最後に使い分け。フィルターの効かせ方は2通りあります。

  • 属性で付ける: [LogActionFilter] をコントローラやアクションに直接。効く範囲が局所的で、コードを見れば「何が効いてるか」が分かる
  • グローバル登録: FilterConfig で全アクションに。認証や例外ログみたいな「全体で漏らしたくない」ものはこっち

判断軸はシンプルで、「1個でも漏れたら困るか」

ログイン確認や例外ログは漏れると事故るのでグローバル。特定画面だけの計測なら属性で十分です。

俺の現場ではこう使ってる

流通系の基幹の保守で、既存コントローラが「アクションごとにログイン確認コピペ」の状態だったことがあります。しかも数箇所、確認が抜けてた。

認証を1個のグローバルフィルターに寄せたら、抜け漏れが構造的に無くなって、コントローラも一気に痩せた。レビューで「ここ確認抜けてない?」の指摘がゼロになったのは、地味に効きました。

一点だけ注意。グローバルにしすぎると「どの画面に何が効いてるか」が読みにくくなります。

認証・例外みたいな全体もの以外は、属性で明示的に付けるほうが後任にやさしいです。

動作確認メモ: この記事のフィルタークラス(System.Web.Mvc 前提)はコンパイルと API の正しさを確認済みです。ただし「認証→アクション→結果」の実際の発火順序やエラー画面の差し替え挙動は、HTTP パイプラインを通す必要があるので、Windows + Visual Studio + .NET Framework の実機でお試しください。

まとめ

  • アクションフィルターは、認証・例外・ログといった 横断的関心事 をアクションの前後に自動で挟む仕組み
  • 4種類(認証・アクション・結果・例外)があり、実行順序は「認証→アクション→結果」、例外は横から拾う
  • 例外フィルターが拾うのは主にアクション中の例外。順序を誤解すると「拾えない」が起きる
  • 握りつぶすなら ExceptionHandled = true。全体もの(認証・例外ログ)はグローバル、局所は属性

横断処理をフィルターに寄せると、コントローラは業務ロジックだけの薄い状態になります。1個直せば全体に効くので、保守がだいぶ楽になります!!こんな感じで、コントローラを薄く保てるのがフィルターの一番の旨みです。

よくある質問

Q1. ASP.NET MVC のフィルターの実行順序は?

認証フィルター(Authorization)が最初、次にアクションフィルターの OnActionExecuting、アクション本体、OnActionExecuted、続いて結果フィルター、View のレンダリングの順です。例外が出ると例外フィルターの OnException が呼ばれます。

Q2. アクションフィルターと例外フィルターの違いは?

アクションフィルター(IActionFilter)はアクションの前後で処理を挟み、ログや実行時間の計測に使います。例外フィルター(IExceptionFilter)は例外が発生したときだけ呼ばれ、例外のログ記録やエラー画面への差し替えに使います。

Q3. フィルターを全アクションに効かせるには?

FilterConfigRegisterGlobalFilters でグローバルフィルターとして登録します。特定のコントローラやアクションだけに効かせたい場合は、属性としてクラスやメソッドに付けます。

Q4. OnActionExecuted で例外を握りつぶしたのに、エラー画面が出るのはなぜ?

filterContext.ExceptionHandled = true を立てていないからです。例外を見てログを吐いても、このフラグを立てないと例外は上流に再スローされ、エラー画面に落ちます。

Q5. 認証チェックはアクションフィルターに書いていい?

認証は認証フィルター(AuthorizeAttribute の継承など)に書くのが筋です。実行順序で認証がアクションより先に走るため、認証をアクションフィルターに書くと「アクションが始まってから弾く」ことになり、無駄な処理が走ります。

次に読むべき記事

以上!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について