HosseinMousavi6486 / ForEvolve.ExceptionMapper

Asp.Net Core middleware that maps Exception types to HTTP Status Code with optional serialization of errors.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ForEvolve.ExceptionMapper

Build, Test, and Deploy

Asp.Net Core middleware that reacts to Exception. You can map certain exception types to HTTP Status Code for example. There are a few predefined exceptions and their respective handler, but you can do whatever you need with this.

All of the handlers are iterated through, in order, so you can build a pipeline to handle exceptions where multiple handlers have a single responsibility. For example, you could have handlers that respond to certain exception types, then one or more fallback handlers that react only if no previous handler handled the exception. Finally, there could be a serializer that convert handled exceptions to JSON, in the format of your choice, making your API linear between endpoints and exception types without much effort.

I plan on adding serialization handlers and Asp.Net Core MVC specific tweaks in a near future.

Versioning

The packages follows semantic versioning. I use Nerdbank.GitVersioning to automatically version packages based on git commits/hashes.

Pre-released

Prerelease packages are packaged code not yet merged to master. The prerelease packages are hosted at feedz.io, thanks to their "Open Source" subscription.

Released

The release and some preview packages are published on NuGet.org.

How to install

Load the ForEvolve.ExceptionMapper NuGet package or one or more of the following if you prefer loading only part of the ExceptionMapper.

List of packages

Name NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.CommonExceptions NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.CommonHttpExceptionHandlers NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.Core NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.FluentMapper NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.HttpMiddleware NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.Scrutor NuGet.org feedz.io
dotnet add package ForEvolve.ExceptionMapper.Serialization.Json NuGet.org feedz.io

ForEvolve.ExceptionMapper

This package references the other packages.

ForEvolve.ExceptionMapper.Core

This package contains the core components like the interfaces, the middleware, etc. You can load only this package to create your own exceptions and handlers.

You can take a look at the samples/WebApiSample project for a working example.

Getting started

You must register the services, and optionally configure/register handlers, and use the middleware that catches exceptions (and that handles the logic).

public void ConfigureServices(IServiceCollection services)
{
    services.AddExceptionMapper(builder => builder
        // Configure your pipeline here
    );
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //...
    app.UseExceptionMapper(); // Register the middleware
    //...
}

Custom exception handler (simple)

If you want to create a custom exception handler that changes the status code and you don't want to use the FluentMapper. Maybe you simply like types or you want to scan one or more assembly to load mappers dynamically; no matter why, you can inherit from ExceptionHandler<TException>.

Let's start by creating an exception:

public class MyForbiddenException : Exception { }

Then create the handler:

public class MyForbiddenExceptionHandler : ExceptionHandler<MyForbiddenException>
{
    public override int StatusCode => StatusCodes.Status403Forbidden;
}

Finally, we can register it:

services.AddExceptionMapper(builder => builder
    .AddExceptionHandler<MyForbiddenExceptionHandler>()
);

If you want a bit more control, you can override Task ExecuteCoreAsync(ExceptionHandlingContext<TException> context) method and add custom code.

Custom exception handler (complex)

If you want to create a custom exception handler, implement IExceptionHandler, then register that handler using the AddExceptionHandler<THandler>() extension method.

In our case, let's create an exception:

public class ImATeapotException : Exception { }

Then the handler:

public class ImATeapotExceptionHandler : IExceptionHandler
{
    public int Order => HandlerOrder.DefaultOrder;

    public async Task ExecuteAsync(ExceptionHandlingContext context)
    {
        var response = context.HttpContext.Response;
        response.StatusCode = StatusCodes.Status418ImATeapot;
        response.ContentType = "text/html";
        context.Result = new ExceptionHandledResult(context.Error);
        await response.WriteAsync("<html><body><pre style=\"font-family: SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;\">");
        await response.WriteAsync(@"             ;,'
    _o_    ;:;'
,-.'---`.__ ;
((j`=====',-'
`-\     /
`-=-'     hjw
Source: <a href=""https://www.asciiart.eu/food-and-drinks/coffee-and-tea"" target=""_blank"">https://www.asciiart.eu/food-and-drinks/coffee-and-tea</a>");
        await response.WriteAsync("</pre></body></html>");
    }

    public Task<bool> KnowHowToHandleAsync(Exception exception)
    {
        return Task.FromResult(exception is ImATeapotException);
    }
}

It is very important to set the context.Result.ExceptionHandled to true. The easiest way to achieve that is to create an instance of the ExceptionHandledResult class.

Once that's done, we can register it:

services.AddExceptionMapper(builder => builder
    .AddExceptionHandler<ImATeapotExceptionHandler>()
);

Assembly Scanning

You can also scan assemblies for IExceptionHandler using IExceptionMappingBuilder extensions. For example, you could scan the Startup assembly for handlers:

services.AddExceptionMapper(builder => builder
    .ScanHandlersFromAssemblyOf<Startup>()
);

This feature uses Scrutor under the hood and exposes part of it to simplify things, so you could use it and use the ScanHandlersFrom() extensions that exposes the Scrutor ITypeSourceSelector, like:

services.AddExceptionMapper(builder => builder
    .ScanHandlersFrom(typeSourceSelector => typeSourceSelector.FromCallingAssembly())
);

ForEvolve.ExceptionMapper.CommonExceptions

This package implements different common exceptions and their handlers, like:

  • 404 NotFound (ForEvolve.ExceptionMapper.NotFoundException)
  • 409 Conflict (ForEvolve.ExceptionMapper.ConflictException)
  • 500 InternalServerError (ForEvolve.ExceptionMapper.InternalServerErrorException)
  • 501 NotImplemented (System.NotImplementedException)

It also comes with a fallback handler that convert unhandled exceptions to 500 InternalServerError. This is an opt-in feature, configured by the FallbackExceptionHandlerOptions. That handler can be useful, but be careful of what you share about your exceptions in production, this could help a malicious user acquire information about your server.

To use the prebuilt handlers, you have to:

services.AddExceptionMapper(builder => builder
    .MapCommonHttpExceptionHandlers()
);

You can also configure the FallbackExceptionHandlerOptions during the registration or like any other options:

services.AddExceptionMapper(builder => builder
    .MapCommonHttpExceptionHandlers(options =>
    {
        options.Strategy = FallbackStrategy.Handle;
    })
);
// OR using the Asp.Net Core options pattern like:
services.Configure<FallbackExceptionHandlerOptions>(options =>
{
    options.Strategy = FallbackStrategy.Handle;
});

ForEvolve.ExceptionMapper.FluentMapper

This package contains utilities that can be used to program exception's mapping in the Startup class, without creating any new type.

For example, to map a MyUnauthorizedException to a status code 401, we could do the following:

services.AddExceptionMapper(builder => builder
    .Map<MyUnauthorizedException>(map => map.ToStatusCode(401))
);

The MyUnauthorizedException looks as simple as this:

public class MyUnauthorizedException : Exception { }

The Map<TException>() extension has been designed to be fluent, so it returns an IExceptionMappingBuilder, allowing us to chain multiple calls. We can also mix and match with any other configuration helpers as they all do that.

For example, we could do the following:

services.AddExceptionMapper(builder => builder
    .MapCommonHttpExceptionHandlers()
    .AddExceptionHandler<ImATeapotExceptionHandler>()
    .Map<MyUnauthorizedException>(map => map.ToStatusCode(401))
    .Map<GoneException>(map => map.ToStatusCode(410))
);

Under the hood, the Map<TException>() extension creates a FluentExceptionHandler<TException> that is configurable. You can append, prepend or replace handler actions.

ForEvolve.ExceptionMapper.Serialization.Json

This package contains a handler that serializes exceptions as ProblemDetails.

The following serializes all exception using the default options:

services.AddExceptionMapper(builder => builder
    // ...
    .SerializeAsProblemDetails()
);

Configuring the serialization handler

It is possible to configure the ProblemDetailsSerializationOptions like any other options, or using the following extension method:

public class Startup
{
    public IConfiguration Configuration { get; }
    // ...
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddExceptionMapper(builder => builder
            // ...
            .SerializeAsProblemDetails(Configuration.GetSection("ExceptionMapper"))
        );
    }
    // ...
}

Here is an example of appsettings.json:

{
    "ExceptionMapper": {
        "SerializeExceptions": true,
        "ContentType": "application/problem+json",
        "JsonSerializerOptions": {
            "IgnoreNullValues": true
            // ...
        }
    }
}

Another overload is to pass an instance of ProblemDetailsSerializationOptions directly like this:

public void ConfigureServices(IServiceCollection services)
{
    var options = new ProblemDetailsSerializationOptions();
    services.AddExceptionMapper(builder => builder
        // ...
        .SerializeAsProblemDetails(options)
    );
}

Release notes

2.0

  • Drop .NET Core 3.1 support
  • Add support for .NET 6.0

1.1

  • Add a handler that serializes exceptions to ProblemDetails (JSON)
  • Add the ForEvolve.ExceptionMapper.Serialization.Json project

1.0

  • Initial release (no yet released)

Future/To do

Here is a list of what I want to do:

  • Take the fallback out of MapCommonHttpExceptions() into its own extension, like MapHttpFallback()
  • Add one or more serialization handlers that at least support JSON serialization and that leverage ProblemDetailsFactory to create ProblemDetails objects.
  • Write tests that covers ForEvolve.ExceptionMapper.FluentMapper and other missing pieces
  • Create a MVC/Razor Pages filter that could replace the middleware or work in conjunction of it, adding more control over the process for MVC applications. This may not be important.

Found a bug or have a feature request?

Please open an issue and be as clear as possible; see How to contribute? for more information.

How to contribute?

If you would like to contribute to the project, first, thank you for your interest, and please read Contributing to ForEvolve open source projects for more information.

Contributor Covenant Code of Conduct

Also, please read the Contributor Covenant Code of Conduct that applies to all ForEvolve repositories.

About

Asp.Net Core middleware that maps Exception types to HTTP Status Code with optional serialization of errors.

License:MIT License


Languages

Language:C# 100.0%