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+).
NOTEBà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:
- Controller phình to — mỗi endpoint thêm ~10 dòng boilerplate try-catch.
- Code lặp — cùng một logic catch được copy-paste qua hàng chục action.
- 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ận | Phiên bản .NET | Độ linh hoạt | Ghi chú |
|---|---|---|---|
| Built-in Middleware | Tất cả | Thấp | Nhanh, đơn giản |
| Custom Middleware | Tất cả | Cao | Toàn quyền kiểm soát pipeline |
| IExceptionHandler | .NET 8+ | Cao | Chuẩ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.
Đă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.
