GlobalExceptionHandler.NET version 2 released
Posted on Friday, 8th December 2017
For anyone that regularly reads this blog will remember that recently I developed a convention based ASP.NET Core exception handling library named GlobalExceptionHandler.NET (if you missed the post you can read about it here).
GlobalExceptionHandler.NET in a nutshell
GlobalExceptionHandler.NET hands off ASP.NET Core’s .UseExceptionHandler()
endpoint and enables developers to configure HTTP responses (including the status codes) per exception type.
For instance, the following configuration:
app.UseExceptionHandler().WithConventions(x => {
x.ContentType = "application/json";
x.ForException<RecordNotFoundException>().ReturnStatusCode(HttpStatusCode.NotFound)
.UsingMessageFormatter((ex, context) => JsonSerializer(new {
Message = ex.Message
}));
});
app.Map("/error", x => x.Run(y => throw new RecordNotFoundException("Record not be found")));
Will result in the following output if a RecordNotFoundException
is thrown.
HTTP/1.1 404 Not Found
Date: Sat, 25 Nov 2017 01:47:51 GMT
Content-Type: application/json
Server: Kestrel
Cache-Control: no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Expires: -1
{
"Message": "Record not be found"
}
Improvements in Version 2
Whilst the initial version of GlobalExceptionHandler.NET was a good start, there were a few features and internal details that I was keen to flesh out and improve, so Version 2 was a pretty big overhaul. Let’s take a look at what’s changed in version 2.
It now extends the UseExceptionHandler()
API
The first version of GlobalExceptionHandler.NET had its own implementation of UseExceptionHandler which would catch any exceptions thrown further down the ASP.NET Core middleware stack, there was no real motivation for creating a separate implementation other than I didn’t realise how extensible the UseExceptionHandler
API was.
As soon as I realised I could offload some of work to ASP.NET Core I was keen to do so.
Now invoked with WithConventions
Now I was using the UseExceptionHandler()
endpoint, I was keen to have a more meaningful fluent approach to integrating with ASP.NET Core, so I ultimately went with a WithConventions()
approach as the name felt a lot more natural.
Supports polymorphic types
One problem the previous version of GlobalExceptionHandler.NET had was it couldn’t distinguish between exceptions of the same type. Version 2 will now look down the inheritance tree for the first matching type. To give an example.
Given the following exception type:
public class ExceptionA : BaseException {}
// startup.cs
app.UseExceptionHandler().WithConventions(x => {
x.ContentType = "application/json";
x.ForException<BaseException>().ReturnStatusCode(HttpStatusCode.BadRequest)
.UsingMessageFormatter((e, c) => JsonSerializer(new {
Message = "Base Exception response"
}));
});
app.Map("/error", x => x.Run(y => throw new ExceptionA()));
HTTP/1.1 400 Bad Request
Date: Sat, 25 Nov 2017 01:47:51 GMT
Content-Type: application/json
Server: Kestrel
Cache-Control: no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Expires: -1
{
"Message": "Base Exception response"
}
As the above example will hopefully illustrate, as there is no configured response to ExceptionA
GlobalExceptionHandler.NET goes to the next type in the inheritance tree to see if a formatter is specified for that type.
Content Negotiation
GlobalExceptionHandler.NET verion 2 now supports content negotiation via the optional GlobalExceptionHandler.ContentNegotiation.Mvc package.
Once included you no longer need to specify the content type or response serialisation type:
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore().AddXmlSerializerFormatters();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler().WithConventions(x =>
{
x.ForException<RecordNotFoundException>().ReturnStatusCode(HttpStatusCode.NotFound)
.UsingMessageFormatter(e => new ErrorResponse
{
Message = e.Message
});
});
app.Map("/error", x => x.Run(y => throw new RecordNotFoundException("Record could not be found")));
}
Note how we had to include the AddMvcCore
service, this is because ASP.NET Core MVC is required in order to take care of content negotiation which is a real shame as it would have been great to enable it without a dependency being required on MVC.
Now when an exception is thrown and the consumer has provided the Accept
header:
GET /api/demo HTTP/1.1
Host: localhost:5000
Accept: text/xml
The response will be formatted according to the Accept
header value:
HTTP/1.1 404 Not Found
Date: Tue, 05 Dec 2017 08:49:07 GMT
Content-Type: text/xml; charset=utf-8
Server: Kestrel
Cache-Control: no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Expires: -1
<ErrorResponse
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Message>Record could not be found</Message>
</ErrorResponse>
Wrapping up
Hopefully this has given you a good idea of what you’ll find in version 2 of the GlobalExceptionHandler.NET library. Moving forward there are a few further improvements I’d like to make around organising configuration on a domain by domain basis. And as always, the code is up on GitHub so feel free to take a look.
Enjoy this post? Don't be a stranger!
Follow me on Twitter at @_josephwoodward and say Hi! I love to learn in the open, meet others in the community and talk Go, software engineering and distributed systems related topics.