きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

ASP.NET core × 例外ハンドラー × チュートリアルメモ

Purpose

ASP.NET core Web APIで例外をハンドルする方法を習得する。その1。

内容について

ASP.NET Core Web API のエラーを処理するの方法を中心に、例外ハンドルの方法をまとめる。

躓いた点

初めに、今回チュートリアルを進める中で躓いた点をまとめておく。

Problemが参照できない。

RFC7807に準拠したレスポンスを返すメソッド(Problem)がMicrosoft.AspNetCore.Mvc.ControllerBaseクラスにある。サンプルコードでも以下のように実装されている。

[ApiController]
public class ErrorController : ControllerBase
{
    [Route("/error")]
    public IActionResult Error() => Problem();
}

この、Problemメソッドが、新規作成したASP. NET core Web APIのプロジェクトの中で参照できなかったAPIリファレンスによると、Microsoft.AspNetCore.Mvc.Core.dllに含まれているとなっているが、最新の2.2.5を取得しても参照ができなかった。
もしかしてと思い、Visual Studio 2019をVisual Studio Installerから最新のものに更新したら、参照できるようになった。
できるようになったが、それはそれで気になる。

チュートリアルメモ

Step0 前提設定

今回は、デフォルトで作成されたValue ControllerのGetメソッドにエラー処理を実装した。このエラーのハンドリングを調べていく。

public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        throw new Exception("Test");
    }
}

Step1 ノーガード戦法

何もエラーをハンドリングしない場合の例。

f:id:kinakomotitti:20210201003035p:plain
ノーガード戦法

Step2 開発者例外ページ

開発時にエラーが発生した際、スタックとレースが見やすい形にまとめられてHTML形式で表示してくれるページ。プロジェクトを作成したときにデフォルトで設定してくれているもの。Startup.csのConigureメソッドの中で設定する。ASPNETCORE_ENVIRONMENT環境変数が、Developmentの場合に利用されるように設定されている。環境変数DevではIsDevelopmentでfalse判定されるので、設定する際は注意する。

 public void Configur(IApplicationBuilder app,IWebHostEnvironment env)
 {
     if (env.IsDevelopment())
     {
         app.UseDeveloperExceptionPage();
     }
     ・・・一部省略
 }

Web APIを開発する際、Postmanなどのツールを使う際には、この画面を表示させると便利とのこと。そういった発想がなかったが、確かにJson形式でドバーっと出るのよりは便利そう。

f:id:kinakomotitti:20210201003106p:plain
DeveloperExceptionPage

Step3 例外処理ミドルウェアの利用

例外処理ミドルウェアを実装・設定することで、エラーのレスポンスを生成する。UseExceptionHandlerを利用することで、エラーが発生した際、指定されたControllerの処理を呼び出してレスポンスを生成することができる。   まずは、ControllerBaseから派生したErrorControllerを作成する。そのクラスに、Errorメソッドを定義する。エラーメソッドで呼び出しているProbremメソッドは、RFC7807に準拠したレスポンスを作成してくれる。
RFC7807は、Machine-Readable(機械が自動的に読み込んで処理できる、Json,CSVなどのデータ形式)なエラーメッセージのフォーマットを定義したもの。OverViewしか見ていないけど、そんな感じ。
これをかえすことで、良い感じのエラー情報が格納されたJson形式のレスポンスが返せるようになる。

public class ErrorController : ontrollerBase
{
    [Route("/error")]
    public IActionResult Error()
    {
        return Problem();
    }
}

次に、このクラスをエラーハンドラーに設定する。

public void ConfigureationBuilder app, Environment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    ・・・一部省略
} 

これで、実行した結果、以下のレスポンスが取得できた。

{
    "type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
    "title":"An error occured while processing your request.",
    "status":500,"traceId":"|74155397-478b92438715b063."
}

typeには、RFCのドキュメントのURLが、Titleには、自分で仕込んだエラーをラップしたエラーのメッセージが表示されていた。あとは、HTTP Statusと、スレッドIDが設定されていた。
今回発生させたエラーには、Testというメッセージが設定されている。これを表示させようとしたら、次のStepの実装が必要になる。

Step4 開発環境用のより詳しいエラーレスポンスの追加

Step2で設定したUseDeveloperExceptionPageメソッドの代わりに、app.UserExceptionHandlerに新しいエラーハンドラーを設定する。チュートリアルに従い、error-local-developmentとする。
まずは、error-local-developmentを定義する。

[ApiController]
public class ErrorController : ControllerBase
{
    [Route("/error-local-development")]
    public IActionResult ErrorLocalDevelopment(
        [FromServices] IWebHostEnvironment webHostEnvironment)
    {
        if (webHostEnvironment.EnvironmentName != "Development")
        {
            throw new InvalidOperationException(
                "This shouldn't be invoked in non-development environments.");
        }

        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();

        return Problem(
            detail: context.Error.StackTrace,
            title: context.Error.Message);
    }
}

このようにすると、発生したエラーの中から、好きな情報を設定して、レスポンスとして返却することができる。StackTraceを出しているので、ドバーっとテキストが表示される。目に悪い。

ここまででまだチュートリアル半分。先は長い。。。

Problemとは・・・

次に進む前に、Problemについて調べておく。以下にControllerBase.Problemのコードを抜粋してみた。

[NonAction]
public virtual ObjectResult roblem(
    string detail = null,
    string instance = null,
    int? statusCode = null,
    string title = null,
    string type = null)
{
    ProblemDetails problemDetails;
    if (ProblemDetailsFactory == null)
    {
        // ProblemDetailsFactory may be null in unit testing scenarios. Improvise to make this more testable.
        problemDetails = new ProblemDetails
        {
            Detail = detail,
            Instance = instance,
            Status = statusCode ?? 500,
            Title = title,
            Type = type,
        };
    }
    else
    {
        problemDetails = ProblemDetailsFactory.CreateProblemDetails(
            HttpContext,
            statusCode: statusCode ?? 500,
            title: title,
            type: type,
            detail: detail,
            instance: instance);
    }
    return new ObjectResult(problemDetails)
    {
        StatusCode = problemDetails.Status
    };
}

ProblemDetailsFactoryを自作して、サービスに登録(DI)したら、それを利用したProbremDetailsが作られ、そうでない場合は、引数で指定した値を使ってProbremDetailsが生成される・・・

と、思ったが、ProblemDetailsFactoryを設定していない場合は、デフォルトのものが利用されるみたいなので、nullになることはない。

f:id:kinakomotitti:20210201003138p:plain
ProblemDetailsFactory
コメントにあるように、Unitテストのシナリオ用に残っているみたいである。確かではないが・・・。
ProblemDetailsFactoryの実装は、Step8くらいで出てくるので、一旦放置する。
その他としては、デフォルトの設定では、nullになってしまうため、レスポンスに出てこないInstanceという属性がある。

問題の特定の発生を識別する URI 参照。参照された場合、さらなる情報が得られることもあれば、得られないこともある。

とあるので、エラー発生時のURLを設定すればいいのか・・・な。