tekitoumemo’s diary

思ったことを書くだけ。長文版Twitter

ASP.NET Core MVCのエラーハンドリング【MVC編】

みんなの洋楽ランキングにて.Net Coreのエラーハンドリングを実装する必要がありました。エラーハンドリングのやり方は公式でも載ってるのですが、実用的な記事があまりないので僕なりのやり方をここで紹介しようと思います ※これは私のオリジナールも含まれているのであくまで参考程度に

例外フィルター

MVC5でもあったOnExceptionを使用します。MVC5ではフィルターじゃなくても実装出来ましたが公式がフィルターの例を出しているのでその例を以下に記載します。

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly IHostingEnvironment _hostingEnvironment;
    private readonly IModelMetadataProvider _modelMetadataProvider;

    public CustomExceptionFilterAttribute(
        IHostingEnvironment hostingEnvironment,
        IModelMetadataProvider modelMetadataProvider)
    {
        _hostingEnvironment = hostingEnvironment;
        _modelMetadataProvider = modelMetadataProvider;
    }

    public override void OnException(ExceptionContext context)
    {
        if (!_hostingEnvironment.IsDevelopment())
        {
            // do nothing
            return;
        }
        var result = new ViewResult {ViewName = "CustomError"};
        result.ViewData = new ViewDataDictionary(_modelMetadataProvider,context.ModelState);
        result.ViewData.Add("Exception", context.Exception);
        // TODO: Pass additional detailed data via ViewData
        context.Result = result;
    }
}

MVC5とほぼ一緒です。_hostingEnvironment.IsDevelopment()はDevelopモードの場合はなにもしないという意味です。私の場合、こんなにいろいろ必要なかったのでめちゃくちゃシンプルにしました。

public override async void OnException (ExceptionContext context) {
    var mailManager = new MailManager {
        Subject = "[みんなの洋楽ランキング]エラー報告",
        Body = string.Format ("{0}\n{1}", context.Exception, context.Exception.StackTrace)
    };
    await mailManager.SendEmailAsync ();
    context.Result = new ViewResult { ViewName = "CustomError" };
}

MVCで例外が発生したらメールを送るだけです。メールの送信方法は以下を参考にしてください。
tekitoumemo.hatenablog.com

これでコントローラーに以下のフィルターを追加するとこちらが動きます。

[CustomExceptionFilter]

OnExceptionは例外を拾ってくれるのですが404とか拾ってくれないので以下のコードをStartup.csにDIします。

if (env.IsDevelopment ()) {
     app.UseDeveloperExceptionPage ();
} else {
     app.UseExceptionHandler ("/Home/Error");
}
app.UseStatusCodePagesWithReExecute("/Home/Error");
app.UseDeveloperExceptionPage

Depelopモードの際にエラーをブラウザに出力します。こちらは2.1ならプロジェクト作成したときに自動生成されます。
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/error-handling/_static/developer-exception-page.png?view=aspnetcore-2.0

app.UseExceptionHandler

例外が発生した際に引数で指定したURLのハンドラを実行します。こちらも2.1ならプロジェクト作成したときに自動生成されます

app.UseStatusCodePagesWithReExecute

元のステータスコードをクライアントに返します。引数で指定したURLのハンドラも実行します。こちらは自動生成されないのでDIしなければいけません。

これで400番台のエラーの対応も出来ました。次にエラー用のアクションメソッドを用意します。

エラー用のアクションメソッド

こちらはデフォルトで出来ている( 2.1なら)はずなので僕の実装したコードを記載します。

public IActionResult Error () {

    switch (HttpContext.Response.StatusCode) {
        case StatusCodes.Status404NotFound:
    // 404の処理
            break;
        case StatusCodes.Status500InternalServerError:
    // 500の処理
            break;
    }
    return View ();
}

HttpContext.Response.StatusCodeからHTTPステータスコードを取得できるのでステータスコードに応じたビューモデルの生成などの処理を入れていけば良いと思います。

処理の順番

Controllerで例外が発生したらどの順番で動くか検証しました。

Controller(例外!!)→例外フィルター→UseExceptionHandlerで指定したURL

例外フィルタの動作は以下のようです。Controllerで補足したエラーをフィルターが拾ってそのあとにUseExceptionHandlerが動く感じですかね。
https://docs.microsoft.com/ja-jp/aspnet/core/mvc/controllers/filters/_static/filter-pipeline-1.png?view=aspnetcore-2.0

これらの実装が正しいのかわかりませんが、公式を見て自分なりに実装した感じではこれが妥当かなと思いました。
次のブログでは起動時の例外処理、つまりMVC以外でエラーが起きたときのエラーハンドリングを紹介しようかと思います。こちらは公式の説明がシンプルなので割と参考になると思います。公式の説明は以下です。

captureStartupErrors と detailedErrors キーを利用し、起動中のエラーに対するホストの動作を構成できます。

雑すぎぃ!!

参考
ASP.NET Core のエラーを処理する | Microsoft Docs
ASP.NET Core フィルター | Microsoft Docs