Setting up an Exception Mapper in .NET Core 2.0
27th November, 2018
  • Rik Boeykens

  • Software Engineer

Handle all exceptions your application throws at you.

In my previous blog post I explained how to set up a custom middleware for my .NET Core 2.0 application. I’d like to expand on this by setting up an exception mapper that will automatically handle all exceptions thrown in the application.

Wrapping the Invoke Method

If our application throws a NullReferenceException or an UnauthorizedException that is not handled properly, we’d just get a 500 status code back. We could set up error handling in each individual call, but thanks to our custom middleware we can do this for all calls from one location.

We start from the custom middleware set up in the previous blog post. We already have an Invoke method where all calls pass through, so we wrap this in a try – catch block:

public async Task Invoke(HttpContext context)
{
	try
	{
		var token = GetToken(context);
		var url = GetDisplayUrl(context);
		_logger.LogInformation($"{token ?? "Unknown user"} is accessing {url}");
		await _next(context);
	} catch(Exception ex)
	{
	}
}

Now whenever an error occurs in our application, it will end up in the catch block. When this happens, we’d like to:

  1. Get the correct error to return based on the exception
  2. Return the error

Returning the Correct Error

We’ll first focus on an ExceptionHandler to return the correct error based on an exception. 

public class ExceptionMapper : IExceptionMapper
{
	private readonly Dictionary<Type, Error> _errorMappings = new Dictionary<Type, Error>
	{
		{
			typeof(UnauthorizedAccessException),
			new Error
			{
				Title = "Unauthorized access",
				Code = "UNAUTH",
				Status = (int)HttpStatusCode.Unauthorized
			}
		},
		{
			typeof(NullReferenceException),
			new Error
			{
				Title = "Not found",
				Code = "NOTFOUND",
				Status = (int)HttpStatusCode.NotFound
			}
		},
		{
			typeof(Exception),
			new Error
			{
				Title = "General error",
				Code = "GENERAL",
				Status = (int)HttpStatusCode.InternalServerError
			}
		}
	};
}

I created a new class Error that holds basic information about the error: title, code and status. I set up a dictionary called ErrorMappings, which is where I can set up the specific errors I’d like mapped to a type of Exception. If an exception is thrown that is not mapped, we fall back to our default Exception type with a general error code.

Next, we’ll create a Resolve method to return the correct error for an exception.

public Error Resolve(Exception ex)
{
	Type exceptionType = ex.GetType() ?? typeof(Exception);
	if(!_errorMappings.TryGetValue(exceptionType, out var error))
	{
		error = _errorMappings[typeof(Exception)];
	}
	return error;
}

I initially try to get the type of the exception. If I can’t, the default type is returned. Then I try to get the corresponding error from the dictionary, and get the default one if it’s not found.

We use this ExceptionMapper in our ExceptionHandler:

public class ExceptionHandler : IExceptionHandler
{
	private readonly IExceptionMapper _exceptionMapper;
	public ExceptionHandler(
	IExceptionMapper exceptionMapper
	){
		_exceptionMapper = exceptionMapper;
	}

	public async Task HandleAsync(HttpContext context, Exception ex)
	{
		var error = _exceptionMapper.Resolve(ex);

		context.Response.StatusCode = error.Status;
		await context.Response.WriteAsync(JsonConvert.SerializeObject((obbject) new { error }));
	}
}

We come into the HandleAsync method from our custom middleware and add in the HttpContext and the exception that was returned. We get the correct error from the ExceptionMapper, then amend the response with the information. As we have the HttpContext, we can amend the response that’s returned. In this case we overwrite the StatusCode and return the serialized error as the response.

The only thing left to do is to call the ExceptionHandler from the custom middleware:

} catch(Exception ex)
{
	await _exceptionHandler.HandleAsync(context, ex);
}

 Now whenever an error is returned, we get an object with some basic information and the correct status code. For example, a NullReferenceException returns the following:

{"error":{"Title":"Not found","Code":"NOTFOUND","Status":404}}

An added benefit to this is that the exact error is shielded from the end user, which makes it safer and more clear. The code we return can be handled on the front end to give a specific, user-friendly error. Whenever I want to handle a different error, I can easily add it to the ExceptionMapper class. The code is available on Github.