Use. NET 6 Developing TodoList Application--Implementing Global Exception Handling

Series Navigation

demand

Because a variety of domain or system exceptions are thrown in a project, a complete try-catch is required in Controller and the return value is repackaged based on whether or not there are exceptions thrown. This is a mechanical and tedious job. Is there a way for the framework to do this on its own?

Yes, the name of the solution is called global exception handling, or how to make the interface fail gracefully.

target

We want exception handling and message returns to be unified within the framework, free from try-catch blocks in the Controller layer.

Principles and ideas

Generally speaking, there are two ideas for global exception handling, but the starting point is through. Implemented by Middleware Pipeline, a pipeline middleware for the NET Web API. The first way is through. NET built-in middleware; The second is a fully customized middleware implementation.

We'll give you a brief introduction on how to implement it with built-in middleware, then actually use the second way to implement our code, so you can compare the differences.

Create the Models folder and ErrorResponse class in the Api project.

  • ErrorResponse.cs
using System.Net;
using System.Text.Json;

namespace TodoList.Api.Models;

public class ErrorResponse
{
    public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.InternalServerError;
    public string Message { get; set; } = "An unexpected error occurred.";
    public string ToJsonString() => JsonSerializer.Serialize(this);
}

Create the Extensions folder and create a new static class, ExceptionMiddlewareExtensions, to implement a static extension method:

  • ExceptionMiddlewareExtensions.cs
using System.Net;
using Microsoft.AspNetCore.Diagnostics;
using TodoList.Api.Models;

namespace TodoList.Api.Extensions;

public static class ExceptionMiddlewareExtensions
{
    public static void UseGlobalExceptionHandler(this WebApplication app)
    {
        app.UseExceptionHandler(appError =>
        {
            appError.Run(async context =>
            {
                context.Response.ContentType = "application/json";

                var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
                if (errorFeature != null)
                {
                    await context.Response.WriteAsync(new ErrorResponse
                    {
                        StatusCode = (HttpStatusCode)context.Response.StatusCode,
                        Message = errorFeature.Error.Message
                    }.ToJsonString());
                }
            });
        });
    }
}

When configuring the middleware configuration initially, note that the middleware pipeline is sequential, and putting global exception handling in the first step (and also the last step of request return) ensures that it intercepts all exceptions that occur. That is, this location:

var app = builder.Build();
app.UseGlobalExceptionHandler();

Global exception handling is now possible. Next, let's see how to fully customize a middleware for global exception handling. The principle is exactly the same, but I prefer to customize the way the middleware code is organized, which is more concise and straightforward.

At the same time, we want to uniformly wrap the return values in a format, so we define such a return type:

  • ApiResponse.cs
using System.Text.Json;

namespace TodoList.Api.Models;

public class ApiResponse<T>
{
    public T Data { get; set; }
    public bool Succeeded { get; set; }
    public string Message { get; set; }

    public static ApiResponse<T> Fail(string errorMessage) => new() { Succeeded = false, Message = errorMessage };
    public static ApiResponse<T> Success(T data) => new() { Succeeded = true, Data = data };

    public string ToJsonString() => JsonSerializer.Serialize(this);
}

Realization

Create a new Middlewares folder and a new middleware GlobalExceptionMiddleware in the Api project

  • GlobalExceptionMiddleware.cs
using System.Net;
using TodoList.Api.Models;

namespace TodoList.Api.Middlewares;

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;

    public GlobalExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            // You can do related logging here
            await HandleExceptionAsync(context, exception);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = exception switch
        {
            ApplicationException => (int)HttpStatusCode.BadRequest,
            KeyNotFoundException => (int)HttpStatusCode.NotFound,
            _ => (int)HttpStatusCode.InternalServerError
        };

        var responseModel = ApiResponse<string>.Fail(exception.Message);

        await context.Response.WriteAsync(responseModel.ToJsonString());
    }
}

This allows our ExceptionMiddlewareExtensions to be written as follows:

  • ExceptionMiddlewareExtensions.cs
using TodoList.Api.Middlewares;

namespace TodoList.Api.Extensions;

public static class ExceptionMiddlewareExtensions
{
    public static WebApplication UseGlobalExceptionHandler(this WebApplication app)
    {
        app.UseMiddleware<GlobalExceptionMiddleware>();
        return app;
    }
}

Verification

First we need to include our return values in the Controller, for example, CreateTodoList, and other similar modifications:

  • TodoListController.cs
[HttpPost]
public async Task<ApiResponse<Domain.Entities.TodoList>> Create([FromBody] CreateTodoListCommand command)
{
    return ApiResponse<Domain.Entities.TodoList>.Success(await _mediator.Send(command));
}

Remember that we have a Colour attribute on the domain entity of the TodoList, that it is a value object, and that during the assignment we give it the opportunity to throw an UnsupportedColourException, so we use this domain exception to validate global exception handling.

For validation purposes, we can make some modifications to CreaeTodoListCommand to accept a Colour string, as follows:

  • CreateTodoListCommand.cs
public class CreateTodoListCommand : IRequest<Domain.Entities.TodoList>
{
    public string? Title { get; set; }
    public string? Colour { get; set; }
}

// The following code is in the corresponding Handler, omitting the others...
var entity = new Domain.Entities.TodoList
{
    Title = request.Title,
    Colour = Colour.From(request.Colour ?? string.Empty)
};

Starting the Api project, we attempted to create a TodoList with an unsupported color:

  • request

  • response

By the way, to see if the normal return format returns as expected, here is the interface that requests all TodoList collections to return:

You can see that normal and exception return types are already unified.

summary

In fact, another way to achieve global exception handling is through Filter, which you can refer to in this article: Filters in ASP.NET Core Instead of selecting Filter, Middleware is primarily based on simplicity, ease of understanding, and being the first Middleware in the middleware pipeline to effectively cover all component processing, including middleware. The location of the Filter is invoked after the routing middleware is in effect. In practice, both methods are used.

Next we will implement the PUT request.

Reference material

  1. Write custom ASP.NET Core middleware
  2. Filters in ASP.NET Core

Keywords: webapi

Added by flaquito on Tue, 28 Dec 2021 22:05:43 +0200