概要
Web APIを開発している際に、既定となる例外処理を実装することがあると思います。
発生した例外に応じて、HTTPステータスコードを変更したり、エラーメッセージ内容を返却したりという共通処理を、
上手くビジネスロジックから切り離して定義することがポイントになるかと思います。
今回は、ASP.NET Core 及び Owin を用いたミドルウェアを利用して、例外処理を行う方法を紹介します。
環境
- ASP.NET Core
- ASP.NET Web API 2 Owin
ミドルウェア パイプライン
ミドルウェアによるパイプライン処理については MS docs の記載が参考になるかと思います。
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.0
HTTPリクエストに対して、各ミドルウェアが順次処理を行うようなイメージです。
ASP.NET Coreをやっている方にとってはお馴染みの仕組みになりますが、ASP.NET MVC (.NET Framework) 時代から、ASP.NET Coreのフレームワーク周りの既定処理に関しては大きく変更されており、Global.asax や 各種Config.cs で設定していた内容は、ミドルウェアにて設定する形になっています。
例外処理用のミドルウェア実装
例外処理用のミドルウェアを ASP.NET Core用 に実装します。
/// <summary>
/// 基底の例外処理を行うミドルウェアです
/// </summary>
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
/// <summary>
/// ロジック内で発生した例外に応じて処理を行います
/// </summary>
/// <param name="context">HttpContext</param>
/// <param name="ex">Exception</param>
/// <returns>Task</returns>
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
// 例外に応じてHTTPステータスコードを設定します
var code = HttpStatusCode.InternalServerError;
if (ex is DomainException) code = HttpStatusCode.BadRequest;
// エラーメッセージを設定します
var result = JsonConvert.SerializeObject(new {error = ex.Message});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int) code;
return context.Response.WriteAsync(result);
}
}
ミドルウェアでは Invokeメソッド にて、ミドルウェアが実際に処理する内容を記載します。
パイプラインにて発生した例外を捕捉し、例外の内容に応じてHTTPステータスコードを設定しています。
アプリケーションが扱う独自の例外を定義しておき、例外の種類に応じて処理を切り分けるのが良いです。
作成したミドルウェアは Startup.cs にて適用します。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 一部省略・・・
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
ASP.NET Web API 2 Owin でミドルウェアを実装する場合、ほぼ似たような実装になります。
違いとしては OwinMiddleware を継承すること、及び Invokeメソッド内 の実装が若干変わる点です。
/// <summary>
/// 基底の例外処理を行うミドルウェアです
/// </summary>
public class ErrorHandlingMiddleware : OwinMiddleware
{
public ErrorHandlingMiddleware(OwinMiddleware next) : base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
try
{
await Next.Invoke(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
/// <summary>
/// ロジック内で発生した例外に応じて処理を行います
/// </summary>
/// <param name="context">IOwinContext</param>
/// <param name="ex">Exception</param>
/// <returns>Task</returns>
private static Task HandleExceptionAsync(IOwinContext context, Exception ex)
{
// 例外に応じてHTTPステータスコードを設定します
var code = HttpStatusCode.InternalServerError;
switch (ex)
{
case ApiBadRequestException _:
code = HttpStatusCode.BadRequest;
break;
case ApiNotFoundException _:
code = HttpStatusCode.NotFound;
break;
}
// エラーメッセージを設定します
var result = JsonConvert.SerializeObject(new {error = ex.Message});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int) code;
return context.Response.WriteAsync(result);
}
}
作成したミドルウェアは Startup.cs にて適用します。
サンプルでは Ninject を使用していますが、通常のWebApiを利用する場合でも同様です。
public void Configuration(IAppBuilder app)
{
// 一部省略・・・
app.Use<ErrorHandlingMiddleware>();
app.UseNinjectMiddleware(CreateKernel);
app.UseNinjectWebApi(configuration);
}
ただ、上記の設定だけでは上手くいきません。
ASP.NET Web API 2 Owin では基底の例外処理 (ExceptionHandler) が存在するため、アプリケーション内で捕捉されなかった例外はそこで処理されてしまい、追加したミドルウェアで処理を行えません。
素直に ExceptionHandler で処理する方法もありますが、ここではミドルウェアで例外処理を行う場合を模索します。
例外を捕捉しないような ExceptionHandler を独自に定義し、基底の ExceptionHandler を差し替えてしまいます。
/// <summary>
/// 例外を処理せずにスローするExceptionHandlerです
/// </summary>
public class PassthroughExceptionHandler : IExceptionHandler
{
public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
ExceptionDispatchInfo.Capture(context.Exception).Throw();
return Task.CompletedTask;
}
}
public void Configuration(IAppBuilder app)
{
// 一部省略・・・
// ErrorHandlingMiddlewareで例外処理を行うため、既定の例外制御を抑止します
configuration.Services.Replace(typeof(IExceptionHandler), new PassthroughExceptionHandler());
// 一部省略・・・
app.Use<ErrorHandlingMiddleware>();
app.UseNinjectMiddleware(CreateKernel);
app.UseNinjectWebApi(configuration);
}
コードサンプル
サンプルコードを Github にアップしています。
一部、今回の記事と無関係な実装も含まれていますが、ご了承ください。
ASP.NET Core Ver
https://github.com/tYoshiyuki/dotnet-core-mediatr-sampleASP.NET Web API 2 Owin Ver
https://github.com/tYoshiyuki/dotnet-owin-webapi-sample
まとめ
ミドルウェアを利用すると、フレームワーク共通の処理を柔軟、かつ簡潔に実装出来るようになります。
今後は、ASP.NET Coreを利用したWeb開発が主流になって来るかと思いますので、是非扱いに慣れておきたいところですね。