Exception handling is an important aspect of any robust web application. In ASP.NET Core, the process of handling exceptions has become even more streamlined with the use of custom middleware. In this article, we will walk through the steps of how to implement global exception handling in ASP.NET Core 6.
Exception Handling with Try-Catch Block
Here is a very basic code snippet to demonstrate the try-catch exception handling, and it should work effectively. Nevertheless, there are a few drawbacks to this approach when working with large applications.
[ApiController]
[Route("api/books")]
public class BookController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<BookController> _logger;
public BookController(IMediator mediator, ILogger<BookController> logger)
{
_mediator = mediator;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> AddBook(AddBookCommand command)
{
try
{
await _mediator.Send(command);
return StatusCode(StatusCodes.Status201Created);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status400BadRequest);
}
}
}
Let’s consider a scenario where we have a substantial number of controllers in our project, each containing multiple actions. To handle exceptions effectively, we would be required to include a try-catch block in every action within each controller. In certain cases, we might also need to add try-catch blocks to our business logic, services, application layer, and so on. Consequently, our codebase would quickly accumulate an overwhelming number of lines, impacting its cleanliness and overall readability.
Fortunately, there is a more efficient approach we can implement to address the issue above. Custom Middleware.
What is Middleware?
Middleware is a fundamental concept that plays a crucial role in the request processing pipeline. It acts as a bridge between the incoming HTTP request and the application’s response. In simple terms, middleware can be thought of as a series of components that process requests and responses as they flow through the system.
When a request is received by an ASP.NET Core application, it passes through a chain of middleware components, each responsible for handling specific tasks. These tasks can include logging, authentication, routing, caching, and more. Each middleware can either terminate the request-response cycle or pass it along to the next middleware in the pipeline.
Exception Handling with Custom Middleware
Let’s create a record for the exception response since record types are highly suitable for data transfer objects (DTOs).
public record ExceptionResponse(HttpStatusCode StatusCode, string Description);
Here is our implementation for the global exception-handling middleware. As a best practice, we should organize this class within a folder named “Middleware.”
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An unexpected error occurred.");
//More log stuff
ExceptionResponse response = exception switch
{
ApplicationException _ => new ExceptionResponse(HttpStatusCode.BadRequest, "Application exception occurred."),
KeyNotFoundException _ => new ExceptionResponse(HttpStatusCode.NotFound, "The request key not found."),
UnauthorizedAccessException _ => new ExceptionResponse(HttpStatusCode.Unauthorized, "Unauthorized."),
_ => new ExceptionResponse(HttpStatusCode.InternalServerError, "Internal server error. Please retry later.")
};
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)response.StatusCode;
await context.Response.WriteAsJsonAsync(response);
}
}
A step-by-step explanation of how it works
The middleware takes two parameters in its constructor: RequestDelegate next and ILogger logger. The next parameter represents the next middleware in the pipeline and the logger is used to log any exceptions that occur.
The core logic of the middleware is in the InvokeAsync method, which is called when an HTTP request is received by the application.
The InvokeAsync method wraps the execution of the next middleware in a try-catch block. This allows it to catch any exceptions that occur during the execution of the downstream middleware.
If an exception is caught, the middleware calls the HandleExceptionAsync method, passing the HTTP context and the exception as an argument. This method logs the error and decides how to handle the exception and creates an appropriate ExceptionResponse object.
Once the ExceptionResponse is generated, the middleware sets the HTTP response’s content type to JSON and the status code based on the ExceptionResponse. Then, it writes the ExceptionResponse as JSON to the HTTP response, informing the client about the error that occurred.
Register ExceptionHandlingMiddleware
We need to be sure about registering the ExceptionHandlingMiddleware in the request processing pipeline by adding the following line to the program.cs file. Be mindful to place it before other middleware registrations, as the order of middleware matters.
// Global error handler
app.UseMiddleware<ExceptionHandlingMiddleware>();
// Other middleware registrations..
Testing the API using Swagger
[ApiController]
[Route("api/books")]
public class BookController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<BookController> _logger;
public BookController(IMediator mediator, ILogger<BookController> logger)
{
_mediator = mediator;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> AddBook(AddBookCommand command)
{
//...
//Throw exception just for testing purpose
throw new Exception("Something went wrong...");
await _mediator.Send(command);
return StatusCode(StatusCodes.Status201Created);
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<Book>))]
public async Task<IActionResult> GetAllBooks()
{
var books = await _mediator.Send(new GetAllBooksQuery());
return Ok(books);
}
}
Our middleware successfully intercepted an unhandled exception that arose during the processing of an HTTP post request.
Here we can enhance our application’s efficiency by saving pertinent logs, preparing a comprehensive response object, and promptly returning it to the client.
Final Thoughts
Exception handling middleware is a powerful tool in ASP.NET Core that helps in centralizing the handling of unhandled exceptions (Avoiding try-catch blocks everywhere) and providing consistent error responses to the clients.
Happy coding :)
Thanks for reading!
Through my articles, I share Tips & Experiences on web development, career, and the latest tech trends. Join me as we explore these exciting topics together. Let’s learn, grow, and create together!
➕More article about Programming, Careers, and Tech Trends.
Top comments (0)