Benutzerdefinierte Ausnahmebehandlung für die .NET-API

· 5 Min. Lesezeit

Ausnahmen sind schlecht, das wissen wir doch, oder? Aber was ist, wenn wir mit ihnen umgehen müssen?

Was passiert, wenn wir beispielsweise eine Ausnahme bei einer API haben, wird eine Stack-Nachricht angezeigt, die viele Informationen enthält, die wir möglicherweise aus der Antwort entfernen möchten, die unsere Benutzer erhalten.

Für die Demo habe ich eine Dotnet-API erstellt und eine Methode hinzugefügt, die eine Ausnahme auslöst:

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

Wenn wir eine Ausnahme auslösen, sieht das so aus:

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)

Das sieht normal aus, das ist es, was man von einer Ausnahme erhält, aber für mich zeigt es viele Informationen an. Wenn Sie Bibliotheken und ähnliches haben, könnte es jemandem, der versuchen könnte, Dinge zu sehen, sinnvolle Informationen über Ihren Kunden, Ihr Projekt oder andere Dinge anzeigen.

So sieht es auf Swagger aus:

Erstellen einer benutzerdefinierten Ausnahme

Dieser Schritt könnte vermieden werden, da wir wissen, welche Ausnahme ausgelöst wird, in diesem Fall eine Exception, aber für mich ist es besser, Ihre benutzerdefinierten Ausnahmen zu haben, da Sie mehr Kontrolle darüber haben, was Sie auslösen.

In diesem Fall habe ich gerade ein Objekt CustomException erstellt, das von Exception erbt:

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) { }
    }
}

Nachdem wir unsere benutzerdefinierte Ausnahme erstellt haben, aktualisieren wir unsere Methode, um CustomException anstelle von Exception auszulösen:

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

Dies wird vorerst nichts ändern, aber der Stack-Trace wird zeigen, dass das geworfene Objekt ein CustomException statt Exception ist. Schauen Sie sich den Anfang des Stack-Trace an:

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)

Erstellen eines ExceptionFilterAttribute

Microsoft hat uns eine Möglichkeit gegeben, Ausnahmen zu behandeln, nachdem sie ausgelöst wurden. Weitere Informationen finden Sie hier.

Aber bisher lautet die Dokumentation, die sie uns geben:

Ein abstrakter Filter, der asynchron ausgeführt wird, nachdem eine Aktion eine Ausnahme ausgelöst hat. Unterklassen müssen OnException(ExceptionContext) oder OnExceptionAsync(ExceptionContext) überschreiben, aber nicht beide.

Erstellen wir also eines, wir erstellen CustomExceptionFilterAttribute, in dem wir OnException überschreiben:

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);
            }
        }
    }
}

Wie Sie sehen können, werfen wir einen Blick auf ExceptionContext. Wenn die Ausnahme ein Typ von CustomException ist, unternehmen wir etwas.

Dieses Etwas aktualisiert die Antwort und den Statuscode dessen, was wir zurückgeben werden.

Um den Statuscode zu aktualisieren, müssen wir context.HttpContext.Response.StatusCode aktualisieren und um das Ergebnis zurückzugeben, müssen wir den context.Result aktualisieren, indem wir ihm ein Objekt geben, das von ActionResult geerbt wird.

Dies ist ein Filter, das heißt, wir müssen ihm etwas hinzufügen, indem wir [CustomExceptionFilter] hinzufügen.

Verwendung des Filters

Nun replizieren wir die Methode, die wir haben, und fügen diesen Filter hinzu, damit er Maßnahmen ergreift. Unser API-Endpunkt wird am Ende so aussehen:

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!");
        }
    }
}

Wie Sie sehen können, haben wir eine neue Methode namens GetWithExceptionHandler, die dieselbe Logik wie GetWithoutExceptionHandler hat, aber in diesem Fall haben wir der Methode den Filter [CustomExceptionFilter] hinzugefügt.

Das Ergebnis ist das Folgende, nachdem wir die Methode ausgeführt haben. Ich zeige ein Bild an, da der Stack-Trace nicht mehr angezeigt wird:

Damit haben wir eine benutzerdefinierte Ausnahme erstellt, einen Filter, der überschreibt, was passiert, wenn wir eine Ausnahme auslösen und sie für eine Methode verwenden.

Dies kann für viele Zwecke verwendet werden, z. B. zur Protokollierung und zum Erkennen, was, wann und wo der Fehler auftritt.