Error Handling in .NET Core Web API with Custom Middleware

In this article, we will discuss how to handle the Exception in the .Net core web API Globally. Generally, there are multiple ways to do it, but today we will discuss it with Middleware.

What is Middleware?

Middleware is software that's assembled into an app pipeline to handle requests and responses.

  • Chooses whether to pass the request to the next component in the pipeline.
  • Can perform work before and after the next component in the pipeline.
    Request

To handle the exception globally, we need to create a custom middleware.

Steps to create a middleware

1. Create a class with the name GlobalExceptionHandlerMiddleware inherit it from the IMiddleware interface, and add the assembly reference i.e. Microsoft.AspNetCore.Http.

After that will implement the InvokeAsync method, which has 2 parameters HttpContext and RequestDelegate.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace LearnWebAPI.Middlewares
{
    public class GlobalExceptionHandlerMiddleware : IMiddleware
    {
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {              
                //handle exception
            }
        }
    }
}

2. Inside the InvokeAsync method, first add the request delegate and HttpContext inside the try block so that if any exception occurs while processing this request, it will be handled in the catch block.

3. Now register this middleware to ServiceCollection DI in Startup.cs file.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddTransient<GlobalExceptionHandlerMiddleware>();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "LearnWebAPI", Version = "v1" });
    });
}

4. Our middleware is now ready to use. Just we need to add this to the Request pipeline, as shown in the below code snippet.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LearnWebAPI v1"));
    }

    app.UseRouting();
    app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

5. Now, we will throw an exception from the code to check whether the Middleware catch block is called or not.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace LearnWebAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };       

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            throw new ArgumentNullException("Value can't be null or empty");
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Global Exception handling

Here we go, the middleware catch block debugger hit, and we can see the exception thrown above in the controller. It's caught in the catch block. Till here, our basic middleware is ready to capture the exception globally.

6. Now, we will add the additional logic to capture the logs using ILogger.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace LearnWebAPI.Middlewares
{
    public class GlobalExceptionHandlerMiddleware : IMiddleware
    {
        private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;

        public GlobalExceptionHandlerMiddleware(ILogger<GlobalExceptionHandlerMiddleware> logger)
        {
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {
                var traceId = Guid.NewGuid();
                _logger.LogError($"Error occure while processing the request, TraceId : ${traceId}," +
                    $" Message : ${ex.Message}, StackTrace: ${ex.StackTrace}");                
            }
        }
    }
}

Let's execute this code and check the Eventviewer or logs, and we can see the logs are getting captured.

.NET Runtime

7. After running the API from Swagger, in the response, still we see a 200 OK response, but this is not the expected response. We need to update the response status code to 500 Internal Server Error with appropriate error message details.

Weather Forcast

8. Let's modify the Error handling code to return the 500 status code with the detailed error message. For sending the error details, we are using the ProblemDetails class provided by Microsoft to capture A machine-readable format for specifying errors in HTTP API responses.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace LearnWebAPI.Middlewares
{
    public class GlobalExceptionHandlerMiddleware : IMiddleware
    {
        private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;

        public GlobalExceptionHandlerMiddleware(ILogger<GlobalExceptionHandlerMiddleware> logger)
        {
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {
                var traceId = Guid.NewGuid();
                _logger.LogError($"Error occure while processing the request, TraceId : ${traceId}, Message : ${ex.Message}, StackTrace: ${ex.StackTrace}");

                context.Response.StatusCode = StatusCodes.Status500InternalServerError;

                var problemDetails = new ProblemDetails
                {
                    Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
                    Title = "Internal Server Error",
                    Status = (int)StatusCodes.Status500InternalServerError,
                    Instance = context.Request.Path,
                    Detail = $"Internal server error occured, traceId : {traceId}",                    
                };
                await context.Response.WriteAsJsonAsync(problemDetails);
            }
        }
    }
}

Here is the final result of our Error Handling middleware in the swagger, which captured the status code with the error detail.

Execute

Hope this article finds you well. Thanks for reading this.