ASP.NET MVCはじめました~エラー処理
実装
グローバルなエラー処理
補足しきれない例外をすべて補足して、エラー画面を表示します。
エラーコントローラの用意
引数ごとにステータスコードと表示メッセージを設定しました。
■Mvc2App/Controllers/ErrorController.cs
public class ErrorController : Controller { public ActionResult Show(string id) { var model = new ErrorViewModel(); var message = string.Empty; HttpContext.Response.TrySkipIisCustomErrors = true; if (id == "Unauthorized") { HttpContext.Response.StatusCode = 401; message = Resource.GetValue("APP_ERR_002"); } else if (id == "LoginFail") { HttpContext.Response.StatusCode = 403; message = Resource.GetValue("APP_ERR_003"); } else if (id == "NotFound") { HttpContext.Response.StatusCode = 404; message = Resource.GetValue("APP_ERR_004"); } else { HttpContext.Response.StatusCode = 500; message = Resource.GetValue("APP_ERR_001"); } model.SysMessage = message; return View(model); } }
エラービューモデルの用意
ViewModelBaseクラスのシステムメッセージプロパティを使用します。
public class ViewModelBase { public string SysMessage { get; set; } }
■Mvc2App/ViewModels/Error/ErrorViewModel.cs
public class ErrorViewModel : ViewModelBase { }
ビューの用意
■Mvc2App/Views/Error/Show.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Mvc2App.ViewModels.Error.ErrorViewModel>" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> Error </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2><%: Model.SysMessage %></h2> </asp:Content>
ロギングクラスの用意
ログの出力はlog4netを使ったりすることが多いと思いますが、勉強がてら簡単なロギングクラスを手作りしました。
ログレベルを3種類(デバッグ、インフォメーション、エラー)用意して、Web.configファイルの設定とメソッドで出力を制御しています。
■Mvc2App/Common/Logger.cs
public enum LogLevel : int { Debug, Info, Error } public static class Logger { public static void Debug(string log) { if (!CheckLogLevel(LogLevel.Debug)) { return; } else { Write(log, LogLevel.Debug); } } public static void Info(string log) { if (!CheckLogLevel(LogLevel.Info)) { return; } else { Write(log, LogLevel.Info); } } public static void Error(string log) { if (!CheckLogLevel(LogLevel.Error)) { return; } else { Write(log, LogLevel.Error); } } private static void Write(string log, LogLevel level) { var path = HttpContext.Current.Server.MapPath("~/Logs"); var logFile = "System.log"; var absolutePath = Path.Combine(path, logFile); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } using (StreamWriter sw = new StreamWriter(absolutePath, true, Encoding.GetEncoding("Shift-JIS"))) using (TextWriter syncWriter = TextWriter.Synchronized(sw)) { syncWriter.Write(DateTime.Now.ToString()); syncWriter.Write("\t"); switch (level) { case LogLevel.Debug: syncWriter.Write("[Debug]\t"); break; case LogLevel.Info: syncWriter.Write("[Info]\t"); break; case LogLevel.Error: syncWriter.Write("[Error]\t"); break; default: throw new InvalidOperationException(); } var session = HttpContext.Current.Session; if (session != null) { if (session["LogOnUser"] != null) { syncWriter.Write("UserName:" + session["LogOnUser"] + "\t"); } } syncWriter.WriteLine(log); } } private static Boolean CheckLogLevel(LogLevel logLevel) { var lL = System.Configuration.ConfigurationManager.AppSettings["logLevel"]; if (lL == null) { return false; } int level; int.TryParse(lL, out level); if ((LogLevel)level > logLevel) { return false; } return true; } }
log4netのように設定したログレベル以上の内容が出力されます。*1
■Mvc2App/Web.config
<configuration> <appSettings> <!-- 0:Debug / 1:Info / 2:Error --> <add key="logLevel" value="0"/> </appSettings> </configuration>
ルートの定義とグローバルエラーハンドラーの用意
"Error"ルートの定義は/Error/Show/○○を/Error/○○で処理できるように定義しました。
"CatchAll"ルートの定義は補足しきれなかったルーティングをエラーコントローラに引き渡します。
補足しきれない例外をグローバルな例外ハンドラーでキャッチするにはGlobal.asaxのApplication_Errorハンドラーに記述します。ここで補足した例外のメッセージをログに出力するようにしました。
■Mvc2App/Global.asax
public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Error", // ルート名 "Error/{id}", // パラメーター付きの URL new { controller = "Error", action = "Show", id = UrlParameter.Optional } // パラメーターの既定値 ); routes.MapRoute( "Default", // ルート名 "{controller}/{action}/{id}", // パラメーター付きの URL new { controller = "Home", action = "Index", id = UrlParameter.Optional } // パラメーターの既定値 ); routes.MapRoute( "CatchAll", "{*anything}", new { controller = "Error", action = "Show", id = "NotFound" } ); } ... protected void Application_Error(Object sender, EventArgs e) { var exception = Server.GetLastError(); if (exception == null) { return; } System.Diagnostics.Debug.WriteLine(exception.Message); Logger.Error(exception.Message); Logger.Error(exception.StackTrace); if (exception.InnerException != null) { System.Diagnostics.Debug.WriteLine(exception.InnerException.Message); Logger.Error(exception.InnerException.Message); Logger.Error(exception.InnerException.StackTrace); } Server.ClearError(); Response.Redirect("~/Error"); } }
コントローラで例外を発生させる
■Mvc2App/Controllers/HomeController.cs
public ActionResult About() { throw new ArgumentException("グローバルエラー処理テスト"); }
実行結果
/Home/Aboutへ遷移し、ArgumentExceptionを発生させます。
■Mvc2App/Logs/System.log
2014/12/16 23:28:53 [Info] Application Start 2014/12/16 23:28:53 [Debug] Session Start 2014/12/16 23:28:59 [Error] UserName:YOSHIDA グローバルエラー処理テスト
/Home/About/test/123へアクセスするとルーティングの例外でエラーコントローラに制御が移り、エラー画面が表示されます。これは"CatchAll"ルートの定義により/Error/Show/NotFoundが実行されているためです。
コントローラレベルでのエラー処理
コントローラ内での例外であれば、HandleError属性を使用する方法もあります。
カスタムエラーを有効にする
HandleErrorを使うにはcustomErrorsのmodeをOnに設定します。
■Mvc2App/Web.config
<configuration> <system.web> <customErrors mode="On"> </customErrors> </system.web> </configuration>
コントローラのアクションにHandleError属性を指定する
HandleErrorで例外が処理された場合は、Application_Errorハンドラーに到達せずログが出力できないので、例外を投げるときにログ出力するように変更しました。
■Mvc2App/Controllers/HomeController.cs
[HandleError(ExceptionType=typeof(ArgumentException))] public ActionResult About() { try { Hoge();//ArgumentException("グローバルエラー処理テスト"); } catch (ArgumentException e) { Logger.Error(e.Message); Logger.Error(e.StackTrace); throw; } }
ビューの用意
HandleErrorは規定でSharedのErrorビューを表示します。
これはテンプレートで用意されているので、例外メッセージが表示されるように変更しました。
■Mvc2App/Views/Shared/Error.aspx
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>" %> <asp:Content ID="errorTitle" ContentPlaceHolderID="TitleContent" runat="server"> エラー </asp:Content> <asp:Content ID="errorContent" ContentPlaceHolderID="MainContent" runat="server"> <h2> 要求の処理中にエラーが発生しました。 </h2> <div> <%: Model.Exception.Message %> </div> </asp:Content>