.NET API でのカスタム例外処理

· 3分で読める

例外は悪いことです、私たちは知っていますよね?しかし、それらを処理しなければならない場合はどうすればよいでしょうか?

たとえば API で例外が発生すると、ユーザーが受け取る応答から削除したい可能性のある多くの情報を含むスタック メッセージが表示されます。

デモ用に、ドットネット API を作成し、例外をスローするメソッドを追加しました。

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new Exception("This is a custom exception!");
}

例外をスローすると、次のようになります。

System.Exception: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method16(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

これは正常に見えますが、これは例外から得られるものですが、私にとっては、多くの情報が表示されます。ライブラリなどがあれば、クライアント、プロジェクト、その他のものに関する賢明な情報を、何かを見ようとしている人に表示できます。

Swagger では次のように表示されます。

カスタム例外の作成

どのような例外がスローされるか (この場合は Exception] であることがわかっているため、このステップを回避することもできますが、私にとっては、スローするものをより詳細に制御できるため、カスタム例外を持つ方が良いと考えています。

この場合、Exception を継承するオブジェクト CustomException を作成しました。

namespace CustomExceptionHandleDemo.Exceptions
{
    public class CustomException : Exception
    {
        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        public CustomException() { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        public CustomException(string message) : base(message) { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        /// <param cref="Exception" name="inner">Parameter for inner</param>
        public CustomException(string message, Exception inner) : base(message, inner) { }
    }
}

カスタム例外を作成したら、Exception の代わりに CustomException をスローするようにメソッドを更新しましょう。

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new CustomException("This is a custom exception!");
}

現時点では何も変わりませんが、スタック トレースでは、スローされたオブジェクトが Exception ではなく CustomException であることが示されます。スタック トレースの先頭を見てください。

CustomExceptionHandleDemo.Exceptions.CustomException: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method24(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

ExceptionFilterAttribute の作成

Microsoft は、例外がスローされた後に例外を処理する方法を提供してくれました。詳細については、ここ で確認できます。

しかし、これまでのところ、彼らが私たちに提供しているドキュメントは次のとおりです。

アクションが例外をスローした後に非同期で実行される抽象フィルター。サブクラスは OnException(ExceptionContext) または OnExceptionAsync(ExceptionContext) をオーバーライドする必要がありますが、両方をオーバーライドすることはできません。

それでは、作成しましょう。 CustomExceptionFilterAttribute を作成し、その中で OnException をオーバーライドします。

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using CustomExceptionHandleDemo.Exceptions;

namespace CustomExceptionHandleDemo.ExceptionFilterAttributes
{
    public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
    {
        /// <summary>
        /// OnException
        /// </summary>
        /// <param cref="ExceptionContext" name="context">Parameter for context</param>
        public override void OnException(ExceptionContext context)
        {
            if (context.Exception is CustomException)
            {
                context.HttpContext.Response.StatusCode = 500;
                context.Result = new ObjectResult(context.Exception.Message);
            }
        }
    }
}

ご覧のとおり、ExceptionContext を調べています。例外が CustomException のタイプである場合、何らかの処理を行います。

これは、返される応答とステータス コードを更新しています。

ステータス コードを更新するには、context.HttpContext.Response.StatusCode を更新する必要があります。結果を返すには、ActionResult から継承したオブジェクトを与えて context.Result を更新する必要があります。

これはフィルターなので、[CustomExceptionFilter] を追加して何かを追加する必要があることを意味します。

フィルターの使用

ここで、持っているメソッドを複製し、このフィルターを追加してアクションを実行しましょう。API エンドポイントは次のようになります。

using CustomExceptionHandleDemo.ExceptionFilterAttributes;
using CustomExceptionHandleDemo.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace CustomExceptionHandleDemo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        public WeatherForecastController()
        {
        }

        [HttpGet]
        [Route("GetWithoutExceptionHandler")]
        public Task GetWithoutExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }

        [HttpGet]
        [Route("GetWithExceptionHandler")]
        [CustomExceptionFilter]
        public Task GetWithExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }
    }
}

ご覧のとおり、 GetWithExceptionHandler という新しいメソッドがあり、 GetWithoutExceptionHandler と同じロジックを持っていますが、この場合はフィルター [CustomExceptionFilter] をメソッドに追加しています。

メソッドを実行した後の結果は次のとおりです。スタック トレースが表示されなくなったため、画像を表示します。

これにより、カスタム例外、つまりスローおよび例外をメソッドで使用したときに何が起こるかをオーバーライドするフィルターを作成しました。

これは、ログを記録したり、何が、いつ、どこでエラーが発生したかを知るなど、さまざまな目的に使用できます。