How to Handle, Serializing, and Returning Exceptions 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!

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!

using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyCodePad
{
    public class ErrorHandlingMiddleware
    {
        private readonly RequestDelegate _next;

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

        public async Task InvokeAsync(HttpContext context)
        { 
            await _next(context);
        }
    }
}

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:

using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyCodePad
{
    public class ErrorHandlingMiddleware
    {
        private readonly RequestDelegate _next;

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

        public async Task InvokeAsync(HttpContext context)
        { 
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {
                await HandleExceptionAsync(context, ex);
            }
        }

        private Task HandleExceptionAsync(HttpContext context, Exception ex)
        { 
            var result = JsonConvert.SerializeObject(ex);

            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            if (!context.Response.Headers.ContainsKey("Access-Control-Allow-Origin"))
            {
                context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            }

            return context.Response.WriteAsync(result);
        }

    }
}

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:

public class ExceptionViewModel
{
    public string ClassName { get; set; }
    public string Message { get; set; }
    public ExceptionViewModel InnerException { get; set; }
    public List<string> StackTrace { get; set; }
}

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

private ExceptionViewModel GetExceptionViewModel(Exception ex)
{
    return new ExceptionViewModel()
    {
        ClassName = ex.GetType().Name.Split('.').Reverse().First(),
        InnerException = ex.InnerException != null ? GetExceptionViewModel(ex.InnerException) : null,
        Message = ex.Message,
        StackTrace = ex.StackTrace.Split(Environment.NewLine).ToList()
    };
}

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:

private ExceptionViewModel GetExceptionViewModel(Exception ex)
{
    var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    var isDevelopment = environment == Microsoft.AspNetCore.Hosting.EnvironmentName.Development;

    return new ExceptionViewModel()
    {
        ClassName = !isDevelopment ? "Exception" : ex.GetType().Name.Split('.').Reverse().First(),
        InnerException = ex.InnerException != null ? GetExceptionViewModel(ex.InnerException) : null,
        Message = !isDevelopment ? "Internal Server Error" : ex.Message,
        StackTrace = !isDevelopment ? new List() : ex.StackTrace.Split(Environment.NewLine).ToList()
    };
}

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *