先週から ASP.NET MVC を試しています。以前、ASP.NET Web フォーム を試したときには、生成される HTML のあまりの汚さに使う気が失せたのですが、ASP.NET MVC は素直な HTML が生成されるので好感が持てます 🙂
試している中で、エラー等が発生したときの適切な HTTP Status Code を伴った通知とエラー内容のロギングを組み合わせたものがある程度できたので、書いてみます。
プロジェクトを生成(ASP.NET MVC 3 Web アプリケーション、テンプレートは「インターネットアプリケーション」、ビューエンジンは「Razor」を選択してます)後、まずはエラー内容を記録するための DB が必要ですね。
テーブル Error を作成します。
列名 | データ型 | nullを許容 | 備考 |
---|---|---|---|
Id | int | false | 主キー |
Uri | varchar(255) | false | |
Controller | varchar(30) | false | |
Action | varchar(30) | false | |
Exception | nvarchar(100) | false | |
Stack | nvarchar(max) | false | |
Updated | datetime | false |
DB ができたら、データモデルを作成します(参考資料「[C#] #33. フェーズ #1 – アプリケーションの作成」)。
次に、HTTP Status Code を伴った通知とエラー内容のロギングを行うフィルタを実装します。
ルートに Filters フォルダを作成して、作った Filters フォルダの中に ErrorLogAttribute.cs を作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using TestErrorPage.Models;
namespace TestErrorPage.Filters
{
public class ErrorLogAttribute : FilterAttribute, IExceptionFilter
{
// アクション・メソッドで例外が発生した場合に実行
public void OnException(ExceptionContext filterContext)
{
// ExceptionContext オブジェクトが空の場合はエラー
if (filterContext == null)
{
throw new ArgumentNullException("No ExceptionContext");
}
// 例外が HttpException の場合、ログ出力をせず、指定されたレスポンスコードとエラーメッセージを返す
if (filterContext.Exception.GetType() == typeof(HttpException))
{
// 例外を処理済みとみなす
filterContext.ExceptionHandled = true;
// HTTP Status code を設定
filterContext.HttpContext.Response.StatusCode =
(filterContext.Exception as HttpException).GetHttpCode();
// Error ビュー (Shared/ErrorCodeMessage.cshtml) を表示
filterContext.Result = new ViewResult()
{
ViewName = "ErrorCodeMessage",
ViewData = new ViewDataDictionary(filterContext.Exception),
};
return;
}
var _db = new TestErrorPageEntities();
// ルート・パラメータを取得
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(),
Exception = filterContext.Exception.GetType().ToString() + ": " +
filterContext.Exception.Message,
Stack = filterContext.Exception.StackTrace,
Updated = DateTime.Now,
};
// Error オブジェクトの内容をデータソースに反映
_db.Errors.AddObject(err);
_db.SaveChanges();
// 例外を処理済みとみなす
filterContext.ExceptionHandled = true;
// HTTP Status code を設定
filterContext.HttpContext.Response.StatusCode = 500;
// Error ビュー (Shared/Error.cshtml) を表示
filterContext.Result = new ViewResult()
{
ViewName = "Error",
};
}
}
}
次に、このフィルタをすべてのコントローラに適用するように、Global.asax.cs の RegisterGlobalFilters へ記述を追加します。
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
// ErrorLog フィルタをすべてのコントローラへ適用する
filters.Add(new ErrorLogAttribute());
}
次に、エラー通知用のビューを作ります。Views\Shared フォルダの中に ErrorCodeMessage.cshtml を作ります。
@model System.Web.HttpException
@{
ViewBag.Title = "エラー";
}
<h2>
@Model.Message
</h2>
次に、動作確認用のコントローラを作ります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace TestErrorPage.Controllers
{
public class TestController : Controller
{
public ActionResult LogTest()
{
throw new Exception("予期せぬエラーが発生しました");
}
public ActionResult Test400()
{
throw new HttpException(400, "Bad Request");
}
}
}
これで、ブラウザから /Test/LogTest を表示させると、エラー通知とログ記録が行われます。
また、/Test/Test400 を表示させると、エラー通知が行われます。
ただ、この状態だと、存在しないアクションが要求された場合にデフォルトのエラー表示が行われてしまうので、さらに、手を加えます。
ルートの Web.config の<system.web> ~ </system.web> の中に <customErrors> を記述する。
<system.web>
<customErrors defaultRedirect="Error" mode="On">
<error statusCode="404" redirect="Errors/index/404" />
</customErrors>
次に、Errors コントローラを作成する。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using TestErrorPage.Filters;
namespace TestErrorPage.Controllers
{
[ErrorLog]
public class ErrorsController : Controller
{
//
// GET: /Errors/
public ActionResult Index(int id, string aspxerrorpath)
{
string message = "";
if (id == 404)
message = "Not Found: " + aspxerrorpath;
throw new HttpException(id, message);
}
}
}
これで、存在しないアクションが呼び出されても、指定したエラー通知の画面が表示されます。
もしかしたら、もっとクールなやり方があるかもしれません。あったら、指摘していただけると嬉しいです 😉
参考にさせていただいたページ:
[ASP.NET MVC]例外フィルタをカスタマイズするには?[3.5、C#、VB]
I’m out of laegue here. Too much brain power on display!