1225 words
6 minutes
Global Error Handling in ASP.NET Core Web API

Lời mở đầu#

Trong các dự án ASP.NET Core, việc xử lý ngoại lệ(Exception) là một phần quan trọng không thể thiếu để đảm bảo ứng dụng hoạt động mượt mà và phản hồi tốt khi gặp sự cố. Khi nhắc đến xử lý ngoại lệ(Exception), ắt hẳn mọi người mường tượng ngay đến các try-catch block trong từng action của controller. Try-catch block trong từng action là cách tiếp cận truyền thống chúng ta có thể dễ dàng gặp ở trong các projects, tuy nhiên cách tiếp cận này có thực sự tốt, chúng ta hãy cùng tìm hiểu trong bài viết này nhé. Let’s go !!!

Xử lý ngoại lệ(Exception) với Try-Catch block#

Dưới đây là một ví dụ minh họa xử dụng Try-Catch block để xử lý ngoại lệ(Exception). Đoạn code này mục đích để minh họa. Ở đây mình dùng Mediatore và CQRS để xử lý queries, command. Nhưng các bạn chỉ cần focus vào mục đích chính là demo về xử lý exception với Try-Catch để chúng ta cùng có một số phân tích phía sau. (Note: Trong bài viết này mình muốn focus vào Ngoại lệ(Exception), còn ở một bài viết khác mình sẽ chia sẻ thêm về Lỗi(Error) cho anh em nhé)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private ILoggerManager _logger;
    private readonly IScopedMediator _mediator;
    public ValuesController(ILoggerManager logger, IScopedMediator mediator)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Get()
    {
        try
        {
            _logger.LogInfo("Fetching all the Products from ElasticSearch");
            var products = await _mediator.SendRequest(new GetProducts(
                request.SearchString,
                request.Ids,
                request.SortOrder,
                request.SortDirection,
                request.Skip,
                request.Take));
            _logger.LogInfo($"Returning {students.Count} students.");
            return Ok(products);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            return StatusCode(500, "Internal server error");
        }
    }
}

Như các bạn có thể thấy, cách sử dụng Try-Catch block truyền thống này có một vài nhược điểm mà ta có thể dễ dàng nhận ra:

  1. Controller sẽ trở nên rất dài(nhiều dòng) nếu như số lượng action(end-points) nhiều.
  2. Các đoạn code có thể bị lặp đi lặp lại, nếu như chúng ta cùng trả ra một mã lỗi.
  3. Chúng ta sẽ phải dùng đến ctrl + C và ctrl + V rất nhiều vì thường sẽ copy paste khi tạo một enpoint mới. Vậy có cách nào khắc phục những nhược điểm này không? Câu trả lời là có, Global Exception Handling chính là giải pháp, tiếp tục đọc để xem Global Exception Handling giải quyết các nhược điểm của cách tiếp cận truyền thống như nào nhé !!!

Global Exception Handling với Middleware#

Từ những nhược điểm đã nêu bên trên với cách xử lý truyền thống sử dụng Try-Catch block ở tất cả các endpoints, mình đặt ra một số câu hỏi cũng như phân tích như sau:

  • Các ngoại lệ(exception) phát sinh từ các endpoints đều là các instance của các class kế thừa từ class Exception. Các bạn có thể xem chi tiết tại đây (https://learn.microsoft.com/en-us/dotnet/api/system.exception?view=net-8.0). Vậy nghĩa là ta hoàn toàn có thể xử lý các ngoại lệ một cách tập trung được, vì chúng ta đa biết root của nó bắt nguồn từ lớp Exception.
  • Câu hỏi tiếp theo là, vậy có cách nào để đoạn code xử lý tập trung Exception được gọi ở tất cả các endpoints hay không? Và câu trả lời là có, Middleware chính là khái niệm mà ta cần dùng đến. Cho các bạn nào chưa biết về Middleware, mình cũng sẽ có một bài viết riêng về Middleware và các ứng dụng thực tế. Các bạn có thể tham khảo thêm tại doc: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0

Vậy là giải pháp đã có, đến giờ thực hành viết một Middleware để xử lý ngoại lệ rồi. Theo kinh nghiệm của mình, có hai cách thường được sử dụng để phục vụ Global Exception Handling là sử dụng Middleware hoặc sử dụng IExceptionHandler interface từ version .NET 8.

Sử dụng Built-in Middleware#

Với cách thực hiện bằng Middleware, chúng ta lại có thể sử dụng Built-in Middleware hoặc Custom Middleware. Đầu tiên với Built-in Middleware chúng ta sẽ sử dụng với app.UseExceptionHandler Built-in method. Đoạn code bên dưới để đảm bảo SOLID mình có tạo ra một static class là ExceptionMiddlewareExtensions, và có một Extention Method là ConfigureExceptionHandler để sử dụng tại bước tiếp theo. Nếu các bạn nào chưa biết về Middleware(built-in middleware và custom middleware) cũng như Extention Method thì mình sẽ có một bài viết riêng về chúng. Chắc sẽ nằm trong Series về ”.NET foundation”, các bạn hãy follow để đọc thêm nhé!

public static class ExceptionMiddlewareExtensions
{
    public static void ConfigureExceptionHandler(this WebApplication app)
    {
        app.UseExceptionHandler(appError =>
        {
            appError.Run(async context =>
            {
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                context.Response.ContentType = "application/json";
                var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                if (contextFeature != null)
                {
                    app.Logger.LogError($"Something went wrong: {contextFeature.Error}");
                    await context.Response.WriteAsync(text: JsonConvert.SerializeObject(new ErrorDetails()
                    {
                        StatusCode = context?.Response?.StatusCode ?? 500,
                        Message = contextFeature.Error.Message, //"Internal Server Error."
                        StackTrace = contextFeature.Error.StackTrace ?? ""
                    }));
                }
            });
        });
    }
}

Sử dụng trong Config method trong file Startup.cs

app.ConfigureExceptionHandler(logger);

Sử dụng custom Middleware#

Với cách sử dụng custom middleware thì các bước cũng khá giống với cách sử dụng built-in middleware. Khác ở chỗ thay vì sử dụng app.UseExceptionHandler. Chúng ta sẽ tạo ra ExceptionMiddleware class - chi tiết các hàm trong class này thì các bạn sẽ hiểu sau khi đọc bài về Middleware, ở bài viết này mình sẽ không đi sâu vào giải thích từng hàm.

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerManager _logger;

    public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
    {
        _logger = logger;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        await context.Response.WriteAsync(new ErrorDetails()
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error from the custom middleware."
        }.ToString());
    }
}

Tương tự với cách sử dụng built-in middleware mình sẽ tạo ra một extention method để code đảm bảo SOLID hơn.

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<ExceptionMiddleware>();
}

Cuối cùng cũng là sử dụng trong Startup.cs thôi.

app.ConfigureCustomExceptionMiddleware();

Xử lý Global Exception với IExceptionHandler Interface từ .NET 8#

Sử dụng Middleware để xử lý Global Exception là một giải pháp tốt.Tuy nhiên .NET 8 giới thiệu IExceptionHandler một cách tiếp cận chuyên dụng cho xử lý Global Exception. Tạo class CustomExceptionHandler và implement IExceptionHanler Interface.

public class CustomExceptionHandler
    (ILogger<CustomExceptionHandler> logger)
    : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken)
    {
        logger.LogError(
            "Error Message: {exceptionMessage}, Time of occurrence {time}",
            exception.Message, DateTime.UtcNow);

        (string Detail, string Title, int StatusCode) details = exception switch
        {
            InternalServerException =>
            (
                exception.Message,
                exception.GetType().Name,
                context.Response.StatusCode = StatusCodes.Status500InternalServerError
            ),
            ValidationException =>
            (
                exception.Message,
                exception.GetType().Name,
                context.Response.StatusCode = StatusCodes.Status400BadRequest
            ),
            BadRequestException =>
            (
                exception.Message,
                exception.GetType().Name,
                context.Response.StatusCode = StatusCodes.Status400BadRequest
            ),
            NotFoundException =>
            (
                exception.Message,
                exception.GetType().Name,
                context.Response.StatusCode = StatusCodes.Status404NotFound
            ),
            _ =>
            (
                exception.Message,
                exception.GetType().Name,
                context.Response.StatusCode = StatusCodes.Status500InternalServerError
            )
        };

        var problemDetails = new ProblemDetails
        {
            Title = details.Title,
            Detail = details.Detail,
            Status = details.StatusCode,
            Instance = context.Request.Path
        };

        problemDetails.Extensions.Add("traceId", context.TraceIdentifier);

        if (exception is ValidationException validationException)
        {
            problemDetails.Extensions.Add("ValidationErrors", validationException.Errors);
        }

        await context.Response.WriteAsJsonAsync(problemDetails, cancellationToken: cancellationToken);
        return true;
    }
}

Sử dụng trong Program.cs

builder.Services.AddExceptionHandler<CustomExceptionHandler>();

Kết luận#

Vậy là trong bài viết này mình đã giới thiệu với các bạn một vài cách xử lý Ngoại lệ(Exception) trong hệ thống .Net, hy vọng bài viết sẽ giúp ích được cho anh em newbie, Junior một phần nào đó để anh em có thể áp dụng vào project thực tế của mình. Trân trọng và see you guys on next post, thanks !!!!

Global Error Handling in ASP.NET Core Web API
https://www.devwithshawn.com/posts/dotnet-global-exception-handling/
Author
PDXuan(Shawn)
Published at
2024-05-01