Skip to main content
.NET

How to add Global Exception Handling in .NET 6 and 7

Christian Schou

Exceptions occur, and we have to face that as developers. I don't think I have ever seen an application running without making any errors. Exceptions need to be handled to avoid application crashes and how can we do that in a smart way? 🤓 We can add middleware to implement Global Exception Handling in .NET.

You can handle exceptions in many ways while building your application. In this post, I will show you how to add support for global exception handling in a .NET Web API running the latest version of .NET (7) (at the time of writing ✍️) to catch errors at runtime automatically.

Prerequisites

In order to follow along or add global exception handling to your own application, you need the following:

Add Global Exception Handling to .NET

Let's move on to the fun stuff - making some code! 😀 The tutorial is divided into these steps.

  1. Create the Web API from the template by Microsoft.
  2. Install Dependencies.
  3. Add Entity for Books.
  4. Create DbContext and add a connection string for the database.
  5. Add Interface and Implementation for Book repository.
  6. Add Book Controller with REST functionality.
  7. Add custom exceptions and middleware to handle exceptions efficiently.
Connect PostgreSQL to .NET 6 Web API using EF Core
Learn how to connect a PostgreSQL database with a .NET 6 App using Entity Framework Core in a few easy steps.

Create WEB API in Visual Studio IDE

The first thing we have to do is to create a new WEB API application. If you are using Visual Studio Code, you can run this command inside a folder, where you want the solution to be located.

dotnet new webapi -n GlobalExceptionHandling

If you are using Visual Studio, you can do the same or follow the step-by-step wizard in the UI when starting up Visual Studio.

Install Dependencies

dotnet add package Microsoft.EntityFrameworkCore 
dotnet add package Microsoft.EntityFrameworkCore.Design 
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Tools

You can read more about the packages at NuGet.org below:

Let's make a dry run and build the solution to make sure that everything works as expected before implementing any logic in the application.

dotnet build

Add a new entity for the book

This WEB API will be very simple. By the end of the post, we will serve a new endpoint for books. To do this we have to define what a book "looks like".

Create a new folder named Entities and create a new file inside this folder named Book.cs with the following code inside:

namespace GlobalExceptionHandling.Entities;
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int Pages { get; set; }
    public int ISBN13 { get; set; }
    public string Author { get; set; }
}

Add database implementation in the project 💾

To store our books we need a database. I am already running a local PostgreSQL database on my development machine that I will be using for this project. If you need one, you can easily configure it using Docker. See the article below.

How to run a PostgreSQL database using Docker Compose
Learn how to run PostgreSQL the powerful, open source object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance using Docker Compose.

Create a new folder named Data at the root of your project and create a new file inside named ApplicationDbContext.cs with the following code inside:

using GlobalExceptionHandling.Entities;
using Microsoft.EntityFrameworkCore;

namespace GlobalExceptionHandling.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Book> Books { get; set; }
    }
}

Here we use Microsoft.EntityFrameworkCore to inherit from DbContext. After the constructor, we create a new DbSet for our Books so that we are able to store the book values in a database.

Now we have to update appsettings.json to include a connection string for our database. See line number 9 - 10 below.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Default": "Driver={PostgreSQL};Server=IP address;Port=5432;Database=myDataBase;Uid=myUsername;Pwd=myPassword;"
  }
}

Remember to update the connection string to fit the credentials for your database installation.

Finally, we have to register our database context inside Program.cs and update the database. Let's start by registering the database with our connection string in Program.cs. See lines 10 - 11 below.

using GlobalExceptionHandling.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddEntityFrameworkNpgsql().AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

To update our database we have to create a migration and then update the database from that migration. Open your Package Manager Console and perform the following commands in the terminal:

Add-Migration "Initial"
Update-Database

There we go 💪. The database is now reflecting our domain in the WEB API and we are ready to implement our Book Service (the repository)

Create Book Service 📖

Now that we are able to store some books in the database, we also need a way of communicating with the database. Let's create a book service to do this.

Create a new folder at the root of your project named Services and add a new file inside it with the name BookService.cs. Inside BookService we will have both the interface and implementation (because it's a small application and only for demo).

The interface will look like this with REST functionality:

using GlobalExceptionHandling.Entities;

namespace GlobalExceptionHandling.Services
{
    public interface IBookService
    {
        Task<IEnumerable<Book>> GetBooksAsync();
        Task<Book?> GetBookAsync(int id);
        Task<Book> AddBookAsync(Book book);
        Task<Book> UpdateBookAsync(Book book);
        Task<bool> DeleteBookAsync(int id);
    }
}

Inside the same file, just below the interface, we will add the implementation of the interface. The code looks like this, I will explain below:

using GlobalExceptionHandling.Data;
using GlobalExceptionHandling.Entities;
using Microsoft.EntityFrameworkCore;

namespace GlobalExceptionHandling.Services
{
    public interface IBookService
    {
        Task<IEnumerable<Book>> GetBooksAsync();
        Task<Book?> GetBookAsync(int id);
        Task<Book> AddBookAsync(Book book);
        Task<Book> UpdateBookAsync(Book book);
        Task<bool> DeleteBookAsync(int id);
    }

    public class BookService : IBookService
    {
        private readonly ApplicationDbContext _dbContext;

        public BookService(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<Book> AddBookAsync(Book book)
        {
            var addResult = await _dbContext.Books.AddAsync(book);
            await _dbContext.SaveChangesAsync();
            return addResult.Entity;
        }

        public async Task<bool> DeleteBookAsync(int id)
        {
            var book = await _dbContext.Books.FirstOrDefaultAsync(x => x.Id == id);
            var deleteResult = _dbContext.Remove(book);
            await _dbContext.SaveChangesAsync();
            return deleteResult != null ? true : false;
        }

        public async Task<Book?> GetBookAsync(int id)
        {
            return await _dbContext.Books.FirstOrDefaultAsync(x => x.Id == id);
        }

        public async Task<IEnumerable<Book>> GetBooksAsync()
        {
            return await _dbContext.Books.ToListAsync();
        }

        public async Task<Book> UpdateBookAsync(Book book)
        {
            var updateResult = _dbContext.Books.Update(book);
            await _dbContext.SaveChangesAsync();
            return updateResult.Entity;
        }
    }
}

This is a very simple repository implementation and as you probably might have noticed, there is no exception handling. No try-catch? Yeah!

What happens? We use ApplicationDbContext as our database context through dependency injection. Each method is made awaitable (async). Each method is pretty self-explaining due to the few lines of logic and well-named methods, but I will include comments in the source code on GitHub if you would like to read those.

Let's register the book service so we can inject it into our Book Controller in a moment. Open Program.cs and add the last line below.

using GlobalExceptionHandling.Data;
using GlobalExceptionHandling.Services;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddEntityFrameworkNpgsql().AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddScoped<IBookService, BookService>();

...

Add Book Controller

Now it's time to make our books available through an API. Create a new Controller named BookController in your Controllers folder. (you can safely delete the default WeatherForecastController)

Inside BookController we will inject our BookService like below:

using GlobalExceptionHandling.Services;
using Microsoft.AspNetCore.Mvc;

namespace GlobalExceptionHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BookController : ControllerBase
    {
        private readonly IBookService _bookService;

        public BookController(IBookService bookService)
        {
            _bookService = bookService;
        }
    }
}

To make all the REST (Read, Update, Select, Delete) methods available, you can copy and paste the code below into your controller. These will populate endpoints at /api/book/... for the client to consume.

using GlobalExceptionHandling.Entities;
using GlobalExceptionHandling.Services;
using Microsoft.AspNetCore.Mvc;

namespace GlobalExceptionHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BookController : ControllerBase
    {
        private readonly IBookService _bookService;

        public BookController(IBookService bookService)
        {
            _bookService = bookService;
        }

        [HttpGet("get")]
        public async Task<IActionResult> GetAll() 
        {
            var books = await _bookService.GetBooksAsync();
            return Ok(books);
        }

        [HttpGet("get/{id}")]
        public async Task<IActionResult> GetById(int id)
        {
            var book = await _bookService.GetBookAsync(id);
            if (book == null)
            {
                return NotFound();
            }
            return Ok(book);
        }

        [HttpPost("add")]
        public async Task<IActionResult> AddBook(Book book)
        {
            var addResult = await _bookService.AddBookAsync(book);
            return Ok(addResult);
        }

        [HttpPut("update")]
        public async Task<IActionResult> UpdateBook(Book book)
        {
            var updateResult = await _bookService.UpdateBookAsync(book);
            return Ok(updateResult);
        }

        [HttpDelete("delete/{id}")]
        public async Task<bool> DeleteBook(int id)
        {
            return await _bookService.DeleteBookAsync(id);
        }
    }
}

If you would like a detailed explanation of how to make a REST API with ASP.NET Core, I would recommend you to check out my tutorial about that below:

How to build a RESTful Web API using .NET Core and EF Core
Learn how to build your own RESTful Web API with ASP.NET Core and Entity Framework Core (.NET 6) with auto data seeding to MS SQL.

Add Global Exception Handling

Now for the thing that you are actually here for. Implementation of global exception handling in .NET.

Create a new folder named Exceptions at the root of your project. Inside the folder, we will be adding the following exceptions in a new file each:

  • CustomException
  • NotFoundException
  • InternalServerException
  • ConflictException
  • BadRequestException
  • UnauthorizedAccessException

Below are the code snippets for each exception. Each of them will inherit from CustomException. The CustomException is inheriting from Exception and looks like this.

CustomException

using System.Net;

namespace GlobalExceptionHandling.Exceptions
{
    public class CustomException : Exception
    {
        public List<string>? ErrorMessages { get; }

        public HttpStatusCode StatusCode { get; }

        public CustomException(string message, List<string>? errors = default, HttpStatusCode statusCode = HttpStatusCode.InternalServerError)
            : base(message)
        {
            ErrorMessages = errors;
            StatusCode = statusCode;
        }
    }
}

In the model above, we inherit from Exception and declare a few properties. First, we got a list of error messages (if we got more than one), then we can populate a status code for our exception and finally we construct the custom exception with some default values if not specified in the Exception class.

NotFoundException

using System.Net;

namespace GlobalExceptionHandling.Exceptions
{
    public class NotFoundException : CustomException
    {
        public NotFoundException(string message)
            : base(message, null, HttpStatusCode.NotFound)
        {
        }
    }
}

InternalServerException

using System.Net;

namespace GlobalExceptionHandling.Exceptions
{
    public class InternalServerException : CustomException
    {
        public InternalServerException(string message, List<string>? errors = default)
            : base(message, errors, HttpStatusCode.InternalServerError) { }
    }
}

ConflictException

using System.Net;

namespace GlobalExceptionHandling.Exceptions
{
    public class ConflictException : CustomException
    {
        public ConflictException(string message)
            : base(message, null, HttpStatusCode.Conflict) { }
    }
}

You can add as many exceptions as you want. This is just to demonstrate how you can add a new custom exception for your project. Next, I will show you how to dynamically handle the exceptions when they are thrown by your application.

Before we implement our middleware we have to create a new model for the ErrorResult we are going to provide the client requesting our API. Create a new folder named Models at the root of your project and a new class named ErrorResult inside that folder. ErrorResult should contain this code:

namespace GlobalExceptionHandling.Models
{
    public class ErrorResult
    {
        public List<string> Messages { get; set; } = new();

        public string? Source { get; set; }
        public string? Exception { get; set; }
        public string? ErrorId { get; set; }
        public string? SupportMessage { get; set; }
        public int StatusCode { get; set; }
    }
}

Now we have to add some middleware for our global exception handling to work. Create a new folder named Middleware at the root of your project and add a new class inside that folder named ExceptionMiddleware.

When creating custom middleware we can inherit from IMiddleware and implement the InvokeAsync() method to run our middleware on the request delegate.

Because we will serialize our response (if has not already started) we have to install the package Newtonsoft.Json. I like to have logs from my middleware and for that, I have installed another package named Serilog. If you want to learn more about Serilog, you should check out this tutorial.

How to Add logging to ASP.NET Core using Serilog - .NET6
Add Serilog to start logging to ASP.NET Core using Serilog with this easy step-by-step .NET Tutorial about logging using Serilog in .NET6.

Install the packages through the Package Manager Console in Visual Studio.

Install-Package Newtonsoft.Json
Install-Package Serilog

Open up ExceptionMiddlware.cs, and add the following code, I will explain it below.

using GlobalExceptionHandling.Exceptions;
using GlobalExceptionHandling.Models;
using Newtonsoft.Json;
using Serilog;
using Serilog.Context;
using System.Net;

namespace GlobalExceptionHandling.Middleware
{
    public class ExceptionMiddleware : IMiddleware
    {
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            try
            {
                await next(context);
            }
            catch (Exception exception)
            {
                string errorId = Guid.NewGuid().ToString();
                LogContext.PushProperty("ErrorId", errorId);
                LogContext.PushProperty("StackTrace", exception.StackTrace);
                var errorResult = new ErrorResult
                {
                    Source = exception.TargetSite?.DeclaringType?.FullName,
                    Exception = exception.Message.Trim(),
                    ErrorId = errorId,
                    SupportMessage = $"Provide the Error Id: {errorId} to the support team for further analysis."
                };
                errorResult.Messages.Add(exception.Message);

                if (exception is not CustomException && exception.InnerException != null)
                {
                    while (exception.InnerException != null)
                    {
                        exception = exception.InnerException;
                    }
                }

                switch (exception)
                {
                    case CustomException e:
                        errorResult.StatusCode = (int)e.StatusCode;
                        if (e.ErrorMessages is not null)
                        {
                            errorResult.Messages = e.ErrorMessages;
                        }

                        break;

                    case KeyNotFoundException:
                        errorResult.StatusCode = (int)HttpStatusCode.NotFound;
                        break;

                    default:
                        errorResult.StatusCode = (int)HttpStatusCode.InternalServerError;
                        break;
                }

                Log.Error($"{errorResult.Exception} Request failed with Status Code {context.Response.StatusCode} and Error Id {errorId}.");
                var response = context.Response;
                if (!response.HasStarted)
                {
                    response.ContentType = "application/json";
                    response.StatusCode = errorResult.StatusCode;
                    await response.WriteAsync(JsonConvert.SerializeObject(errorResult));
                }
                else
                {
                    Log.Warning("Can't write error response. Response has already started.");
                }
            }
        }
    }
}

Okay, what happens above?

  1. First, we create a new error id using a Guid. The error id and exception stack trace are then pushed as two properties onto our logger (Serilog).
  2. A new ErrorResult object is created using the exception data and the error message is added.
  3. If the exception is not of the type CustomException (the exceptions we made in our Exceptions folder) and the InnerExpetion is not equal to null we set the exception variable to the InnerException to be switched on in a moment.
  4. We then switch on the exception to set error messages and status codes on exceptions that we either declare in the middleware or as a custom exception.
  5. Finally, we log the error using Serilog along with the HTTP Status code and error ID we generated earlier so we can trace it back in the logs from our server. This makes it easier for us in the future to investigate errors our users have encountered.

Now we only have to register our global exception-handling middleware in the project. For this, I have created a new file inside Middleware named Startup.cs. This class is static and is an extension method for ApplicationBuilder. This allows us to inject our middleware in Program.cs.

namespace GlobalExceptionHandling.Middleware
{
    public static class Startup
    {
        public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app)
            => app.UseMiddleware<ExceptionMiddleware>();
    }
}

When injected in Program.cs we can do the following:

app.UseGlobalExceptionHandler();

Usage

The Global Exception Handler is now implemented and ready to use. In the real world, we would throw the exception if we encounter something in our application logic that we do not like or want to happen. Below is an example of how you can use NotFoundException for this demo API.

In BookController.cs you got the REST methods. Imagine we are looking for a book that does not exist in the database. How can we throw an error using one of our newly created custom exceptions?

Below is a change to the GetById endpoint in the controller, where I have removed NotFound() as a response and replaced it with the custom exception for that.

[HttpGet("get/{id}")]
public async Task<IActionResult> GetById(int id)
{
    var book = await _bookService.GetBookAsync(id);
    if (book == null)
    {
        throw new NotFoundException($"A product from the database with ID: {id} could not be found.");
    }
    return Ok(book);
}

You could also place them in the service/repository to handle exceptions there. In many cases that is my approach.

Summary

Global Exception Handling in .NET is a good approach to always return consistent error messages to clients. In this tutorial, you learned how to implement new middleware to handle exceptions thrown by custom exceptions and how you dynamically deal with them and present them in a nicely formatted way for the client.

It is important to handle exceptions. They occur from time to time and if not handled, they will break your application and make it stop. I have seen production applications where this had happened and that can potentially cost a lot of money i lost revenue.

I hope you learned something new from this tutorial about global exception handling in .NET. If you got any issues, questions, or suggestions, please let me know in the comments below. Until next time - Happy coding and thank you for reading! ✌️

Source code

The full source code is available on my GitHub for TWC. You can fork, copy or do what you want at the link below.

GitHub - Tech-With-Christian/Global-Exception-Handling: This repository demonstrates how easy it is to implement global exception handling in .NET
This repository demonstrates how easy it is to implement global exception handling in .NET - GitHub - Tech-With-Christian/Global-Exception-Handling: This repository demonstrates how easy it is to i...