ASP.NET Core Web APIでアプリケーションロジック内では例外を投げておいて、共通処理として例外をキャッチして例外に応じたステータスコードとメッセージのレスポンスを返したい場合があるかと思います。ASP.NET Framework やCore 2.2まであったHttpResponseException
を使うようなユースケースです。
ASP.NET Core 3ではなくなっていますが、自前で作ることでより自分のユースケースにあった処理が作れることが次のドキュメントに記載されています。
今回これをすこしアレンジした使い方をしたので紹介してみます。
まず、この例外をスローしたときにどのような共通処理をしたいかを決めるためにActionFilterの実装であるHttpResponseExceptionFilter
から説明します。OnActionExecuted
メソッド内に処理を記述します。WebAPI(に限らないが)で例外がスローされていた場合、context.Exception
にその例外オブジェクトが格納されます。この例外がHttpResponseExceptionFilter
以外の場合は想定しない例外スローなので適切にログを出力します*1。HttpResponseExceptionFilter
の場合はHttpResponseException例外オブジェクトに格納されている情報をもとにレスポンスを返します。
using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace MyApp { public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter { public int Order { get; set; } = int.MaxValue - 10; public void OnActionExecuting(ActionExecutingContext context) { } public void OnActionExecuted(ActionExecutedContext context) { if (context.Exception is HttpResponseException exception) { context.Result = new ObjectResult(new { is_error = true, message = exception.Message }) { StatusCode = exception.Status, }; context.ExceptionHandled = true; } else if (context.Exception != null) { //TODO HttpResponseException以外の例外がスローされたので適切にログの処理などを行う Console.WriteLine("Unhandled exception!!!"); Console.WriteLine(context.Exception); } } } }
今回は次のようなJSONのメッセージで指定してHTTPステータスで返すことを想定しています。そこで、HttpResponseException
にステータスコードを表すプロパティを設定し、メッセージはException.Message
プロパティを利用することにしました。
{ "is_error": true "message": "<例外メッセージ>" }
そこで、HttpResponseException
にStatus
プロパティを持たせています。あとは使いやすいようにいくつかコンストラクタを作成しておきます。
using System; namespace MyApp { public class HttpResponseException : Exception { public int Status { get; set; } public HttpResponseException(int status, string message, Exception innerException) : base(message, innerException) { Status = status; } public HttpResponseException(int status, Exception innerException) : base(innerException.Message, innerException) { Status = status; } public HttpResponseException(int status, string message) : base(message) { Status = status; } } }
あとはStartupクラスのConfigureServices
メソッド内でHttpResponseExceptionFilter
を有効にします。
public void ConfigureServices(IServiceCollection services) { services.AddControllers(options => options.Filters.Add(new HttpResponseExceptionFilter())); //... }
あとは実際にWebAPIの処理の中でこのように使います。
UserModel user; try { user = await connection.QueryFirstOrDefaultAsync<UserModel>( "SELECT * FROM `users` WHERE `id` = @id", new { id = userID }); } catch (Exception e) { if (txn != null) await txn.RollbackAsync(); throw new HttpResponseException(StatusCodes.Status500InternalServerError, "db error", e); } if (user == null) { if (txn != null) await txn.RollbackAsync(); throw new HttpResponseException(StatusCodes.Status401Unauthorized, "user not found"); }
*1:また起こるべきでない状態なので開発中ならバグ修正、本番なら通知を行うようにするのも必要でしょう