705 từ
4 phút
Global Error Handling in ASP.NET Core Web API

Vấn đề: try-catch tràn lan khắp controller#

Mỗi lần thêm endpoint mới, bạn lại copy-paste cùng một khối try-catch. Controller phình to, logic xử lý lỗi lặp lại ở mọi nơi, và chỉ cần quên một chỗ là API trả về stack trace cho client. Global Exception Handling giúp bạn xử lý lỗi tập trung tại một điểm duy nhất, loại bỏ hoàn toàn try-catch khỏi controller.

Bài viết này đưa ra 3 cách triển khai: Built-in Middleware, Custom Middleware, và IExceptionHandler (.NET 8+).

NOTE

Bài viết tập trung vào Exception (ngoại lệ runtime). Xử lý Error (lỗi domain/validation có chủ đích) sẽ ở một bài riêng.

Try-Catch truyền thống - tại sao không nên dùng#

Xem ví dụ controller điển hì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");
        }
    }
}

3 vấn đề rõ ràng:

  1. Controller phình to — mỗi endpoint thêm ~10 dòng boilerplate try-catch.
  2. Code lặp — cùng một logic catch được copy-paste qua hàng chục action.
  3. Dễ quên — endpoint mới không có try-catch = lộ stack trace ra client.

Giải pháp: chuyển toàn bộ logic xử lý exception ra một chỗ duy nhất.

Cách 1: Built-in Middleware với UseExceptionHandler#

Cách đơn giản nhất — dùng app.UseExceptionHandler có sẵn trong ASP.NET Core.

Tạo extension method để giữ Program.cs sạch:

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,
                        StackTrace = contextFeature.Error.StackTrace ?? ""
                    }));
                }
            });
        });
    }
}

Đăng ký trong Program.cs:

app.ConfigureExceptionHandler(logger);

Khi nào dùng: Dự án nhỏ, cần triển khai nhanh, không cần kiểm soát chi tiết pipeline.

Cách 2: Custom Middleware#

Khi cần kiểm soát nhiều hơn (ví dụ: map exception type sang HTTP status code, enrich log), tự viết middleware.

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());
    }
}

Extension method + đăng ký:

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

Khi nào dùng: Cần custom logic phức tạp, hoặc dự án chưa lên .NET 8.

Cách 3: IExceptionHandler (.NET 8+) — Khuyến nghị#

Từ .NET 8, Microsoft cung cấp IExceptionHandler — interface chuyên dụng cho global exception handling. Ưu điểm so với middleware tự viết: tích hợp sẵn với DI container, hỗ trợ chain nhiều handler, và trả về ProblemDetails chuẩn RFC 7807.

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;
    }
}

Đăng ký trong Program.cs:

builder.Services.AddExceptionHandler<CustomExceptionHandler>();

Khi nào dùng: Dự án .NET 8+. Đây là cách được Microsoft khuyến nghị.

Tổng kết#

Cách tiếp cậnPhiên bản .NETĐộ linh hoạtGhi chú
Built-in MiddlewareTất cảThấpNhanh, đơn giản
Custom MiddlewareTất cảCaoToàn quyền kiểm soát pipeline
IExceptionHandler.NET 8+CaoChuẩn Microsoft, hỗ trợ DI + chain handler

Quy tắc chung: Dù chọn cách nào, controller của bạn không nên có try-catch. Mọi exception xử lý tập trung tại một điểm duy nhất.

Global Error Handling in ASP.NET Core Web API
https://www.devwithxuan.com/vi/posts/dotnet-global-exception-handling/
Tác giả
XuanPD
Ngày đăng
2024-05-01
Giấy phép
CC BY-NC-SA 4.0
Chia sẻ:

Đăng ký nhận bản tin

Nhận thông báo khi có bài viết mới. Không spam, hủy bất cứ lúc nào.

Bình luận