Handling, serializing and returning exceptions with a middleware in an ASP.NET Core API

ASP.NET Core offers a very cool way to avoid all that boring boilerplate exception handling code, just by intercepting all requests. It is called Middleware and actually, ASP.NET Core is full of built-in middlewares!

What is a middleware?

Microsoft docs describe a middleware as:

Middleware is software that’s assembled into an app pipeline to handle requests and responses. Each component:

  • Chooses whether to pass the request to the next component in the pipeline.
  • Can perform work before and after the next component in the pipeline is invoked.

But you can also get a very good idea about a middleware by just that following image:
Request processing pattern showing a request arriving, processing through three middlewares, and the response leaving the app. Each middleware runs its logic and hands off the request to the next middleware at the next() statement. After the third middleware processes the request, the request passes back through the prior two middlewares in reverse order for additional processing after their next() statements before leaving the app as a response to the client.
The three blue middlewares above belong in a pipeline, so the black arrow that represents the request is passing from one middleware to the next. Each middleware intercepts the request in its travel towards the last middleware and can execute custom code before or after the request leaves the middleware. By seeing this image we can also have an idea about how to write our exception handling middleware: Rename Middleware 1 to ErrorHandlingMiddleware and wrap the next(); in a try catch block!

Writing an exception handling middleware

I am not going to get into details about how to write a middleware because you can easily read about it here, and thus, here is an empty useless middleware!

The InvokeAsync method does all the job and calls the _next(); middleware. As theory describes, you can have your code before _next();, you can have it after and you can even decide if you will ever call _next(); (short-circuiting the pipeline).
But isn’t that alone _next(); asking for a try catch block, or what?
Let’s do it, and handle the exception:

Up until this point we have a middleware that can successfully catch any exception, try to serialize it and return it back to the client. That could be the end of this post, but there are three significant problems!

      1. All exceptions implement the ISerializable interface, but this is only suggests that they should be serializable. You can’t be sure they are!
      2. Exceptions have inner exceptions!
      3. Handling all exception types in one try catch block is a bad idea because you can hide unexpected problems. We should only handle the ones that our code is written for.

Serializing exceptions

The accepted answer on Stackoverflow for the question “Are all .NET Exceptions serializable” states:

In order to consider serializability we need to consider both the immediate class and all types which are members of this type (this is a recursive process). The base Exception class has a Data property which is of type IDictionary. This is problematic because it allows you, or anyone else, to add any object into the Exception. If this object is not serializable then serialization of the Exception will fail.

That simply means no! You cannot count on an exception being serializable!
One easy way to forget about serializability problems is to use your own custom ViewModel. Add one to your API and map the Exception properties you want with the properties of your brand new ExceptionViewModel:

And here is the mapping (notice the recursion for the inner exceptions):

Final thoughts

Middlewares, among other, offer an amazing opportunity to create a global handler for our exceptions and return back a serialized version of that exception. This can be very helpful if we want to pass this information to the client, but by doing so we could potentially expose sensitive information and details about our code. The best approach to avoid that would be to use the ASPNETCORE_ENVIRONMENT and stop sending details when it is not a development environment. For example: