How to use DotNET Core Exception Handling Middleware

Let’s look into how we can handle exceptions in a .NET Core Web API with the help of middleware. We will see how we can catch the exception and handle it in one place in our application.

In this article, we will explore the powerful capabilities of exception handling middleware in .NET Core, enabling centralized error handling for a smoother and more robust Web API experience.

The previous article discussed How to implement DotNET Core OAuth2 for an API . Based on this article we will see how we can catch the exceptions and log them into a file.

If you want to find out more about middleware in .NET Core you can read this article from Microsoft.

Table of Contents

Overview

The main target for me will be to handle exceptions in one place and from there to make sure I return a proper response to the client. Besides this, I want to log the exceptions somewhere. Here is a diagram of what I want to achieve:

DotNet-Core-Api-Exception-Handling-Middleware

Creating the middleware

The exception middleware is a class that I added to the main project. Besides the exception middleware class, we also need an extension class. We call the middleware from Program.cs.

ErrorHandlingMiddleware class:

public class ErrorHandlingMiddleware
    {
        private readonly RequestDelegate next;
        private readonly ILogger<ErrorHandlingMiddleware> logger;
        public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
        {
            this.next = next;
            this.logger = logger;
        }
        public async Task Invoke(HttpContext context)
        {
            try
            {
                context.Request.EnableBuffering();
                await next(context);
            }
            catch (Exception exception)
            {
                if (exception is ValidationException || exception is ApplicationException)
                {
                    await HandleBadRequestExceptionsAsync(context, new { message = exception.Message });
                }
                else
                {
                    await HandleExceptionAsync(context, exception);
                }
            }
        }
        private Task HandleExceptionAsync(HttpContext context, Exception exception)
        {
            try
            {
                LogError(context, exception).RunSynchronously();
            }
            catch { }
            var result = JsonConvert.SerializeObject(new { message = "Internal Server Error" });
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            return context.Response.WriteAsync(result);
        }
        private async Task LogError(HttpContext context, Exception exception)
        {
            var errorParams = new Dictionary<object, object>();
            errorParams["user"] = context.User.Identity.Name;
            errorParams["url"] = context.Request.GetEncodedUrl();
            errorParams["method"] = context.Request.Method;
            var headers = string.Empty;
            foreach (var requestHeader in context.Request.GetTypedHeaders().Headers)
            {
                if (requestHeader.Key == "Authorization")
                {
                    headers += $"{requestHeader.Key}: ****** | ";
                    continue;
                }
                headers += $"{requestHeader.Key}: {requestHeader.Value} | ";
            }
            headers = headers.TrimEnd(' ', '|');
            errorParams["headers"] = headers;
            context.Request.Body.Position = 0;
            string body;
            using (var reader = new StreamReader(
                context.Request.Body,
                Encoding.UTF8))
            {
                body = await reader.ReadToEndAsync();
            }
            body = HideSensitiveData(body);
            errorParams["body"] = body;
            logger.LogError(exception, exception.Message, errorParams);
        }
        private string HideSensitiveData(string body)
        {
            if (body.Contains("password"))
            {
                var jsonObject = JObject.Parse(body);
                if (jsonObject["password"] != null)
                {
                    jsonObject["password"] = "******";
                }
                body = JsonConvert.SerializeObject(jsonObject);
            }
            return body;
        }
        private Task HandleBadRequestExceptionsAsync(HttpContext context, object error)
        {
            var result = JsonConvert.SerializeObject(error);
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            return context.Response.WriteAsync(result);
        }
    }

Extension class:

public static class ExceptionExtension
    {
        public static IApplicationBuilder HandleExceptionsAsync(this IApplicationBuilder app)
        {
            app.UseMiddleware(typeof(ErrorHandlingMiddleware));
            return app;
        }
    }

Here is what I tried to achieve with the ErrorHandlingMiddleware class. The first thing I wanted was a way to isolate exception handling in one place. Secondly, I wanted to have the logging of errors done in one place, without spreading it around the application. The third thing was to hide server errors and return a standard response to the client. This way the client won’t know anything about what went wrong on the server side but it will know something went wrong. I would extend this section with a procedure to provide the client with an ID of the error. The ID can be a unique identifier used for a support team to have better tracking of the issue.

In order to be able to split the exceptions between expected exceptions and random exceptions occurring in the application, I chose to use ApplicationException when throwing an expected exception. An example is when the user logs in and provides the wrong username or password. In this case, I throw an ApplicationException with the message “Invalid username or password!”. The exception middleware catches this exception, sees that it is an application exception, and returns 400 Bad Request with the message provided in the exception.

Handling expected exceptions is as straightforward as it can be. These are errors that the client receives information on and what he can do differently to fix them. But what about unexpected exceptions?

As mentioned above, the first thing I wanted was to hide the unexpected exception and return a generic exception message to the client. In my case, I am returning an “Internal Server Error”. Display this message in the UI or it can handle it in any other way.

The next thing was to log the exception somewhere. For the sake of it, I logged the exceptions in a file. We all know that in most cases reproducing errors can be tricky. So I decided to log more info about the request. I found it useful to have info about the URL, user identifier, and of course the body of the request. When logging this information I would recommend hiding sensitive data like passwords.

After you created the middleware and the middleware extension, don’t forget to call it from Program.cs:


app.HandleExceptionsAsync();

If you want to see the exception middleware in action please take a look at my GitHub project https://github.com/relaxedcodercom/IdentityMicroservice . If you have any questions please let me know in the comments section below.