For this blog post, we are going to create a simple ASP.NET Core Web API and then add global exception handling.

What is Global Exception Handling in ASP.NET Core?

Global exception handling is exactly what it sounds like. It lets you handle all of your exceptions globally in one place, rather than having to handle them manually all throughout your applcation. As with lots of things in ASP.NET Core this needs to be configured in the Startup.cs class and added to the Configure method.

Create ASP.NET Core Web API

I am using .NET Core 2.1 for this demo. Using the command line enter:

dotnet new webapi -o GlobalExceptionHandlingDemo

This will create you a new Web API project in a folder called 'GlobalExceptionHandlingDemo'.

Let's see what happens by default if we throw an exception in the Get() method of the ValuesController - which is the controller added for you by default.

// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    throw new Exception();
}

If we then run the app using dotnet run and then visit https://localhost:5001/api/values in our browser or API Development Environment (Postman, Insomnia e.t.c) you will see that we are shown a developer friendly exception page. This might seem like a good enough solution, but what if we want to log some details or perform some clean up code when this happens?

One way of solving this is to use a try-catch statement, so we would change our Get() method above to be:

// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    try
    {
        throw new Exception("Exception with details of our application");
    }
    catch (Exception ex)
    {
        // Log exception - ex
        return StatusCode(500, "The execution of the service failed in some way.");
    }
}

The above try-catch statement works fine, but it means we have to write this code in multiple places, and it also means that we lose our developer friendly error page which is useful for when our environment is Development.

Global Exception Handling

To get the results we are after we want to configure the app.UseExceptionHandler() in our Startup class. The top of our Configure method now looks like this:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseHsts();
    app.UseExceptionHandler(appBuilder => appBuilder.Run(async context => {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsync("The execution of the service failed in some way.");
    }));
}

Consumers of our API do not care nor need to know the specific status codes or error messages that are happening internally in our application, so the above bit of middleware takes any error and changes the status code to 500, and writes out a generic response.

If we then change our Get method back to:

// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    throw new Exception("Exception with details of our application");
}

And make sure that we change our ASPNETCORE_ENVIRONMENT environment variable to anything other than 'Development' then we will still see the generic error message and the 500 Internal Server Error status.

Adding Logging

We can alter our UseExceptionHandler middleware component to also log everytime an exception is thrown and capture the details of the error:

app.UseExceptionHandler(appBuilder => appBuilder.Run(async context => {
    var features = context.Features.Get<IExceptionHandlerFeature>();
    if(features != null)
    {
        var logger = loggerFactory.CreateLogger("Global Exception Handler Logger");
        logger.LogError(500, features.Error, features.Error.Message);
    }
    context.Response.StatusCode = 500;
    await context.Response.WriteAsync("The execution of the service failed in some way.");
    }));

For that to work you need to make sure that an ILoggerFactory is passed into the Configure method. The full Configure method now looks like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole();
    loggerFactory.AddDebug(LogLevel.Trace);

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
        app.UseExceptionHandler(appBuilder => appBuilder.Run(async context => {
            var features = context.Features.Get<IExceptionHandlerFeature>();
            if(features != null)
            {
                var logger = loggerFactory.CreateLogger("Global Exception Handler Logger");
                logger.LogError(500, features.Error, features.Error.Message);
            }
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("The execution of the service failed in some way.");
        }));
    }

    app.UseHttpsRedirection();
    app.UseMvc();
}

Summary

And that's all there is to it. That's a very basic and simple way of adding global exception handling to your ASP.NET Core applciation. For more info, see the middleware docs here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/ and the 'ExceptionHandlerExtensions.UseExceptionHandler Method' docs can be found here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler