.NET TIPS

[ASP.NET MVC]例外フィルタをカスタマイズするには?[3.5、C#、VB]

山田 祥寛
2009/10/15

 「連載:ASP.NET MVC入門 第4回 フィルタ属性による認証/キャッシュ/セキュリティ対策の実装」では、フィルタ属性の1つであるHandleError属性を利用することで、カスタム・エラー・ページを有効化する方法について紹介した。サンプルという便宜上、取りあえずブラウザに直接、例外情報(例外の発生場所やエラー・メッセージ)を表示する例を紹介しているが、一般的にはエンド・ユーザーに例外情報を開示すべきケースは少ない(というよりも、開示すべきではない)。というのも、例外のようなアプリケーションの内部情報はエンド・ユーザーにとっては無意味なものであるのみならず、重要な情報を露出することから重大なセキュリティ・ホールの一因となってしまう可能性もあるためだ。

 実際のアプリケーションでは、このような例外情報は背後でロギングし、エンド・ユーザーに対してはエラーが発生した旨と、場合によってはシステム管理者への連絡先を示すなど、ごくシンプルな情報を表示するにとどめるべきだろう。

 そこで本稿では、例外発生時に例外情報をデータベースに登録するための、カスタムのフィルタ属性(例外フィルタ)であるErrorLog属性を定義してみよう。ErrorLog属性は、例外情報をロギングした後、自動的にビュー・スクリプトであるMyError.aspxファイルを検索&描画するので、(ファイル名は固定されるものの)標準的なHandleError属性と異なり、Web.Configの設定も不要である。

本稿で作成するサンプル
アクション・メソッドで例外が発生した場合に、指定されたエラー・ページが表示される。

例外情報はErrorテーブルに記録される

 それではさっそく、具体的な実装の手順を見ていくことにしよう。

 なお、本サンプル・プログラムを動作させるに当たっては、「連載:ASP.NET MVC入門 第1回 ASP.NET MVCフレームワーク 基本のキ」で紹介した手順に従って、ASP.NET MVCをインストールし、また、「ASP.NET MVC Web Application」プロジェクトを作成しておく必要がある。

1 .データベースを用意する

 ここでは、アクション・メソッドで発生した例外情報をデータベースに登録するものとする。よって、サンプルを動作させるには、データベースに以下のようなErrorテーブルを用意し、Entity Data Model(エンティティ・データ・モデル。以下EDM)にもテーブルに対応したエンティティを追加しておく必要がある。EDMの作成方法については、「連載:ASP.NET MVC入門 第2回 スキャフォールディング機能で軽々DB連携アプリケーション」で解説しているので、参考にしていただきたい。

フィールド名 データ型 概要
Id INT ログコード(主キー/自動連番)
Uri VARCHAR(255) リクエストURI
Controller VARCHAR(30) 例外が発生したコントローラの名前
Action VARCHAR(30) 例外が発生したアクションの名前
Stack NVARCHAR(MAX) スタック・トレース
Updated DATETIME エラー発生日時
Errorテーブルのフィールド・レイアウト

2. 例外フィルタを定義する

 次は本題、例外フィルタを表すErrorLogAttributeクラスを実装する。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcAppCs.Models;

namespace MvcAppCs.Filters {
  public class ErrorLogAttribute : FilterAttribute, IExceptionFilter {

    // アクション・メソッドで例外が発生した場合に実行
    public void  OnException(ExceptionContext filterContext) {

      // ExceptionContextオブジェクトが空の場合はエラー
      if (filterContext == null) {
        throw new ArgumentNullException("No ExceptionContext");
      }
      var _db = new MyMvcEntities();

      // ルート・パラメータを取得
      var route = filterContext.RouteData;

      // Errorオブジェクト(エンティティ)を生成
      // (リクエストURI、コントローラ名、アクション名、
      //  スタック・トレース、エラー発生日時を設定している)
      var err = new Error() {
        Uri = filterContext.HttpContext.Request.RawUrl,
        Controller = route.Values["controller"].ToString(),
        Action = route.Values["action"].ToString(),
        Stack = filterContext.Exception.StackTrace,
        Updated = DateTime.Now
      };
      // Errorオブジェクトの内容をデータソースに反映
      _db.AddObject("Error", err);
      _db.SaveChanges();

      // 例外を処理済みと見なす
      filterContext.ExceptionHandled = true; 

      // MyErrorビュー(Shared/MyError.aspx)を描画
      filterContext.Result = new ViewResult() {
        ViewName = "MyError"
      }; 
    }
  }
}
Public Class ErrorLogAttribute
  Inherits FilterAttribute
  Implements IExceptionFilter

  ' アクション・メソッドで例外が発生した場合に実行
  Public Sub OnException(ByVal filterContext As ExceptionContext) Implements IExceptionFilter.OnException 

    ' ExceptionContextオブジェクトが空の場合はエラー
    If filterContext Is Nothing Then
      Throw New ArgumentNullException("No ExceptionContext")
    End If
    Dim _db As New MyMvcEntities()

    ' ルート・パラメータを取得
    Dim route = filterContext.RouteData

    ' Errorオブジェクト(エンティティ)を生成
    ' (リクエストURI、コントローラ名、アクション名、
    '  スタック・トレース、エラー発生日時を設定している)
    Dim err As New [Error]() With { _
      .Uri = filterContext.HttpContext.Request.RawUrl, _
      .Controller = route.Values("controller"), _
      .Action = route.Values("action"), _  ' アクション名
      .Stack = filterContext.Exception.StackTrace, _  ' スタック・トレース
      .Updated = DateTime.Now _  ' エラー発生日時
    }
    ' Errorオブジェクトの内容をデータソースに反映
    _db.AddObject("Error", err)
    _db.SaveChanges()

    ' 例外を処理済みと見なす
    filterContext.ExceptionHandled = True 

    ' MyErrorビュー(Shared/MyError.aspx)を描画
    filterContext.Result = New ViewResult() With { _
      .ViewName = "MyError" _
    } 
  End Sub
End Class
ロギング機能付きの例外フィルタ(上:ErrorLogAttribute.cs、下:ErrorLogAttribute.vb)

 コードの大まかな流れはリスト内のコメントを参照いただくとして(Entity Frameworkに関する部分は、「特集:Visual Studio 2008 SP1新機能解説(2) .NETの新データアクセス・テクノロジ『ADO.NET Entity Framework』」を参考されたい)、ここでは例外フィルタにかかわるいくつかのポイントにフォーカスしてみよう。

例外フィルタはFilterAttibuteクラスを継承、IExceptionFilterインターフェイスを実装すること

 FilterAttributeクラス(System.Web.Mvc名前空間)はフィルタ属性の基本的な機能を提供する基底クラス、IExceptionFilterインターフェイス(System.Web.Mvc名前空間)は例外フィルタに必要なメソッドを定義したインターフェイスである。例外フィルタを実装するには、これらのクラス/インターフェイスを継承/実装したクラスを定義する必要がある。

 IExceptionFilterインターフェイスで実装しなければならないメソッドは、唯一、OnExceptionメソッドだけだ。OnExceptionメソッドはアクション・メソッドで例外が発生した場合に呼び出されるメソッドで、本稿の例のように、例外情報のロギングやカスタム・エラー・ページの呼び出しに利用することができる。

 OnExceptionメソッドは引数としてExceptionContextオブジェクトを受け取る。例外情報をはじめ、例外の発生時に呼び出されていたコントローラ/アクションの情報、そのほか、ルート・パラメータなどには、すべてExceptionContextオブジェクトを介してアクセスが可能である。

例外が処理済みであることを通知する

 例外フィルタ(OnExceptionメソッド)で例外を処理した場合には、例外が処理済みであることを明示的に通知する必要がある。これには、ExceptionContextオブジェクトのExceptionHandledプロパティにtrueをセットするだけでよい。

 もしもExceptionHandledプロパティがfalse(デフォルト)である場合には、例外は未処理であると見なされ(例外が再スローされ)、この場合はそのまま標準のエラー・ページが表示されてしまうので、注意が必要だ。

処理後の結果を指定する

 例外フィルタでの処理を終えた後、最終的な出力方法を決めるのはExceptionContextオブジェクトのResultプロパティの役割だ。

 Resultプロパティには、ActionResult派生クラスのいずれかのインスタンスを設定することができる。ActionResult派生クラスについては、「連載:ASP.NET MVC入門 第3回 ActionResultオブジェクトでアクション操作も自由自在」を併せて参照していただくとよいだろう。

 ここでは、ViewResultクラスをインスタンス化することで、強制的にビュー・スクリプトを呼び出しているわけだ。呼び出すビュー・スクリプトを設定するには、ViewNameプロパティを指定すればよい。ここでは「MyError」を呼び出しているので、Viewsフォルダ配下の、

(1)コントローラ名/MyError.aspx
(2)コントローラ名/MyError.ascx
(3)Shared/MyError.aspx
(4)Shared/MyError.ascx

が順に検索され、描画されることになる。ちなみに、例外情報をビュー変数経由で渡したいならば、ViewDataプロパティで設定することも可能だ。

3. アクション・メソッド&エラー・ページを用意する

 例外フィルタが用意できたところで、実際に動作を確認してみよう。

 ここでは、意図的に例外を発生するFilter/LogTestアクションと、例外発生時に呼び出されるMyError.aspxファイルを用意する。なお、MyError.aspxファイルは通常はアプリケーション共通で利用するのが一般的と思われることから、ここではViews/Sharedフォルダに配置するものとする。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using MvcAppCs.Filters;

namespace MvcAppCs.Controllers {
  public class FilterController : Controller {

    // 例外フィルタであるErrorLog属性を宣言
    [ErrorLog()]
    public ActionResult LogTest() {
      throw new Exception("予期せぬエラーが発生しました。");
    }
  }
}
Public Class FilterController
  Inherits System.Web.Mvc.Controller

  ' 例外フィルタであるErrorLog属性を宣言
  <ErrorLog()> _
  Function LogTest() As ActionResult
    Throw New Exception("予期せぬエラーが発生しました。")
  End Function

End Class
意図的に例外を発生させるFilter/LogTestアクション(上:FilterController.cs、下:FilterController.vb)
例外フィルタに固有の話ではないが、属性を宣言する場合には、クラス名の末尾のAttributeは省略可能である。つまり、ErrorLogAttributeであれば、単にErrorLogと記述できる。

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
  <title>カスタムエラーページ</title>
</head>
<body>
  <div>
  予期せぬエラーが発生しました。<br />
  しばらく待ってアクセスし直すか、システム管理者にご連絡ください。
  </div>
</body>
</html>
カスタム・エラー・ページを表すビュー・スクリプト(Shared/MyError.aspx*
* エラー・ページの内容はほとんど静的なコンテンツであるため、ここではC#版のコードのみを掲載する。

 以上を理解したら、

http://localhost:8080/Filter/LogTest

のようなアドレスで、実際にサンプルを起動してみよう。冒頭の画面のように、カスタム・エラー・ページが表示され、かつ、Errorテーブルには例外情報が記録されていれば、ErrorLogフィルタは正しく動作している。End of Article

利用可能バージョン:.NET Framework 3.5
カテゴリ:ASP.NET MVC 処理対象:例外
使用ライブラリ:FilterAttributeクラス(System.Web.Mvc名前空間)
使用ライブラリ:IExceptionFilterインターフェイス(System.Web.Mvc名前空間)

この記事と関連性の高い別の.NET TIPS
WPF:例外をまとめてトラップするには?[C#/VB]
[ASP.NET AJAX]非同期通信時に発生した例外情報をロギングするには?
適切に処理されなかった例外をキャッチするには?
[ASP.NET AJAX]非同期通信で発生した例外の処理方法を変更するには?
async/awaitで例外処理をするには?[C#/VB]
このリストは、(株)デジタルアドバンテージが開発した
自動関連記事探索システム Jigsaw(ジグソー) により自動抽出したものです。
generated by

「.NET TIPS」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間