Felhantering (ASP.NET WebApi serie 3/4)

I likhet med den förra posten i den här serien, där vi tittade på loggning, kommer felhanteringen som beskrivs i den här posten lösas på två olika sätt, i tre olika valda tekniska omgivningar, ganska lika dom för loggning:

  • Felhantering mha IExceptionHandler (.NET Framework)
  • (OWIN) Middleware (.NET Framework)
  • ASP.NET Core Middleware

Målet med felhanteringen är att på ett tydligt och enhetligt sätt presentera ett svar till konsumenten av api:et som är lätt att förstå och avhjälpa och lätt att spåra för utvecklare vid felsökning eller support.

I dom tre fallen nedan kommer felen som presenteras för konsumenten att härröra från ett känt exception, IExceptionForOutsideWorld eller liknande. Detta interface och en implementation kan se ut enligt nedan.

interface IExceptionForOutsideWorld  
{
    HttpStatusCode HttpStatusCode { get; }
    string Message { get; }
}

public class SafeForWorldOutsideException : Exception, IExceptionForOutsideWorld  
{
    public HttpStatusCode HttpStatusCode => HttpStatusCode.BadRequest;
    public override string Message => "This error is for demo purposes";
}

Låt oss nu börja med IExceptionHandler för det fulla .NET-ramverket.

IExceptionHandler (.NET Framework)

I det fulla .NET-ramverket och dess WebApiConfig-klass så kan man registrera en, för webbapplikationen, global exception-hanterare via HttpConfiguration-objektets kollektion av services:

public static class WebApiConfig  
{
   public static void Register(HttpConfiguration config)
   {
      ...

      config.Services.Replace(typeof(IExceptionHandler), new CustomExceptionHandler());

      ...
   }

   ...
}

Klassen CustomExceptionHandler ska implementera IExceptionHandler, metoden public Task HandleAsync. I exemplet nedan kontrolleras så att det kastade felet är av typen IExceptionForOutsideWorld och då kan felsvaret tillbaka till api-konsumenten fyllas på med relevant information.

public class CustomExceptionHandler : IExceptionHandler  
{
   public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
   {
      var exception = context.Exception;
      if (exception is IExceptionForOutsideWorld)
      {
         var responseException = exception as IExceptionForOutsideWorld;

         context.Result =
            new ExceptionForOutsideWorldResult(
               responseException.HttpStatusCode,
               responseException.Message,
               context.Request);
      }

      return Task.FromResult(0);
   }
}

Resultatet av det felhanterade anropet byggs med hjälp av klassen ExceptionForOutsideWorldResult:

public class ExceptionForOutsideWorldResult : IHttpActionResult {  
    private readonly HttpStatusCode _statusCode;
    private readonly string _message;
    private readonly HttpRequestMessage _request;

    public ExceptionForOutsideWorldResult(HttpStatusCode statusCode, string message, HttpRequestMessage request)
    {
        _statusCode = statusCode;
        _message = message;
        _request = request;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(_request.CreateErrorResponse(_statusCode, _message));
    }
}

Titta på VehiclesController för att se hur ett SafeForWorldOutsideException rakt av kastas och fastnar i felhanteraren:

[RoutePrefix("api/vehicles")]
public class VehiclesController : ApiController  
{
    ...

    [HttpGet]
    [Route("throw")]
    public IHttpActionResult Throw()
    {
        throw new SafeForWorldOutsideException();
    }

    ...
}

Den kompletta källkoden för det här fallet återfinns här https://github.com/HeadlightAB/LoggingAndExceptionHandlingASPNetWebApi/tree/master/FullFrameworkLoggingUsingFilter.

(OWIN) Middleware (.NET Framework)

Det här fallet där man använder en OWIN-pipeline i sin webbapplikation känns som att lägga på en enhetlig felhantering med hjälp av ett OWIN Middleware borde vara väldigt trivialt och rakt på. Så visar det sig inte vara, vilket gör att någon sådan lösning inte presenteras här.

Det allra enklaste är att implementera exakt samma lösning som beskrevs ovan, genom att registrera en IExceptionHandler-service i WebApiConfig. Källkoden för denna här lösningen finns här https://github.com/HeadlightAB/LoggingAndExceptionHandlingASPNetWebApi/tree/master/FullFrameworkLoggingUsingOwin.

ASP.NET Core Middleware

En enkel felhantering i ASP.NET Core byggs lika enkelt och på samma sätt som audit-loggningen beskriven i del 2 i den här serien, http://blog.headlight.se/loggning-post-2#core-framework-logging-filter.

I Startup.cs byggs request-response pipeline på med ett exceptionhandling middleware enligt nedan:

public class Startup  
{
    ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseLoggingMiddleware(); // Logging using middleware

        app.UseExceptionHandlingMiddleware(); // Exception handling

        app.UseMvc();
    }
    ... 
}

Notera ordningen på loggning- och felhanteringsmiddleware. I det här exemplet kommer audit-loggning alltid att utföras så länge som potentiellt fel hanteras korrekt i excpetionhanteringen

Implementationen av middleware ser ut enligt nedan, där även extensionmetoden till IApplicationBuilder UseExceptionHandlingMiddleware visas överst:

public static class ExceptionHandlingMiddlewareExtensions  
{
    public static IApplicationBuilder UseExceptionHandlingMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

public class ExceptionHandlingMiddleware  
{
    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception exception)
        {
            if (exception is IExceptionForOutsideWorld responseException)
            {
                context.Response.Clear();
                context.Response.StatusCode = (int) responseException.HttpStatusCode;
                context.Response.ContentType = MediaTypeNames.Text.Plain;

                await context.Response.WriteAsync(responseException.Message);
            }
        }
    }
}

Även här bygger exemplet på att fel som ska hanteras och formatteras korrekt tillbaka till konsumenten av API:et ska vara av typen IExceptionForOutsideWorld.

Komplett kod för projektet i det här fallet återfinns här https://github.com/HeadlightAB/LoggingAndExceptionHandlingASPNetWebApi/tree/master/NetCoreLogging.

Tillbaka till introduktionen.