Chapter 02
Exception Handling
Deep Dive
IExceptionHandler, ProblemDetails, Custom Exceptions — xử lý lỗi bài bản
IExceptionHandler (.NET 8+)
Đây là cách được khuyến nghị từ .NET 8. DI-friendly, có thể chain nhiều handlers, dễ test.
C#
public class GlobalExceptionHandler : IExceptionHandler { private readonly ILogger<GlobalExceptionHandler> _logger; public GlobalExceptionHandler( ILogger<GlobalExceptionHandler> logger) => _logger = logger; public async ValueTask<bool> TryHandleAsync( HttpContext httpContext, Exception exception, CancellationToken ct) { _logger.LogError(exception, "Unhandled exception: {Message}", exception.Message); var (statusCode, title) = exception switch { NotFoundException => (404, "Not Found"), ValidationException e => (400, "Validation Failed"), ForbiddenException => (403, "Forbidden"), UnauthorizedAccessException => (401, "Unauthorized"), _ => (500, "Internal Server Error") }; httpContext.Response.StatusCode = statusCode; await httpContext.Response.WriteAsJsonAsync(new ProblemDetails { Status = statusCode, Title = title, Detail = exception.Message, Instance = httpContext.Request.Path }, ct); return true; // true = exception đã được xử lý } }
Đăng ký trong Program.cs
C#
var builder = WebApplication.CreateBuilder(args); // Đăng ký exception handler và ProblemDetails builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); builder.Services.AddProblemDetails(); var app = builder.Build(); // ĐẶT ĐẦU TIÊN trong pipeline app.UseExceptionHandler(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
TryHandleAsync trả về true nếu đã xử lý xong exception. Trả false để pass
cho handler tiếp theo trong chain. Bạn có thể đăng ký nhiều IExceptionHandler — chúng chạy theo
thứ tự đăng ký.
Custom Exception Middleware (Pre-.NET 8)
Với .NET 6/7 hoặc khi cần toàn quyền kiểm soát:
C#
public class ExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ExceptionMiddleware> _logger; public ExceptionMiddleware( RequestDelegate next, ILogger<ExceptionMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); // Gọi middleware tiếp theo } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception"); await HandleExceptionAsync(context, ex); } } private static async Task HandleExceptionAsync( HttpContext context, Exception ex) { context.Response.ContentType = "application/problem+json"; context.Response.StatusCode = ex switch { NotFoundException => 404, ValidationException => 400, _ => 500 }; await context.Response.WriteAsJsonAsync(new ProblemDetails { Status = context.Response.StatusCode, Title = ex.GetType().Name, Detail = ex.Message }); } } // Đăng ký: app.UseMiddleware<ExceptionMiddleware>();
✔
IExceptionHandler (.NET 8+)
Cách mới, khuyến nghị
- DI-friendly (constructor injection)
- Chain nhiều handlers
- Tích hợp sẵn với ProblemDetails
- Ít boilerplate code
⚙
Custom Middleware
Cách cũ, toàn quyền kiểm soát
- Hoạt động mọi version .NET
- Toàn quyền kiểm soát response
- Chỉ một handler duy nhất
- Nhiều boilerplate hơn
Exception Filters
Exception Filters chỉ hoạt động trong phạm vi MVC pipeline — từ Resource Filters đến Action execution:
Auth Filters
ngoài scope
→
Resource
trong scope
→
Action
trong scope
→
Exception Filter
bắt tại đây
C#
public class ApiExceptionFilter : IExceptionFilter { private readonly ILogger<ApiExceptionFilter> _logger; public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger) => _logger = logger; public void OnException(ExceptionContext context) { _logger.LogError(context.Exception, "Exception in {ActionName}", context.ActionDescriptor.DisplayName); context.Result = new ObjectResult(new ProblemDetails { Status = 500, Title = "Internal Server Error", Detail = context.Exception.Message }) { StatusCode = 500 }; context.ExceptionHandled = true; } } // Đăng ký global: // builder.Services.AddControllers(opt => // opt.Filters.Add<ApiExceptionFilter>());
Exception Filters KHÔNG bắt: exception từ middleware (CORS, Auth), model binding errors, routing
errors. Luôn kết hợp với
UseExceptionHandler() làm lưới an toàn cuối cùng.
ProblemDetails (RFC 7807)
Chuẩn quốc tế cho error response trong API. Thay vì format tùy tiện, mọi lỗi đều có cấu trúc thống nhất:
DON'T — Format tùy tiện
JSON
{
"error": "Not found",
"success": false
}
Mỗi API format khác nhau. Client phải xử lý nhiều dạng.
DO — ProblemDetails
JSON
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Not Found",
"status": 404,
"detail": "Product with ID 999 was not found",
"instance": "/api/products/999",
"traceId": "00-abc123..."
}
Chuẩn RFC 7807. Mọi API cùng format. Content-Type: application/problem+json
C#
// Tùy chỉnh ProblemDetails cho mọi response builder.Services.AddProblemDetails(opt => { opt.CustomizeProblemDetails = ctx => { ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier; ctx.ProblemDetails.Extensions["nodeId"] = Environment.MachineName; }; });
Custom Exception Classes
Tạo exception hierarchy rõ ràng giúp pattern matching trong handler trở nên đơn giản:
AppException ← Base exception cho ứng dụng
├── NotFoundException → 404
├── ValidationException → 400 (kèm danh sách errors)
├── BusinessRuleException → 422 (vi phạm business rule)
├── ForbiddenException → 403
└── ConflictException → 409 (duplicate, race condition)
├── NotFoundException → 404
├── ValidationException → 400 (kèm danh sách errors)
├── BusinessRuleException → 422 (vi phạm business rule)
├── ForbiddenException → 403
└── ConflictException → 409 (duplicate, race condition)
C#
public abstract class AppException : Exception { public abstract int StatusCode { get; } protected AppException(string message) : base(message) { } } public class NotFoundException : AppException { public override int StatusCode => 404; public NotFoundException(string entity, object id) : base($"{entity} with ID {id} was not found") { } } public class ValidationException : AppException { public override int StatusCode => 400; public IDictionary<string, string[]> Errors { get; } public ValidationException( IDictionary<string, string[]> errors) : base("One or more validation failures occurred") => Errors = errors; } public class BusinessRuleException : AppException { public override int StatusCode => 422; public BusinessRuleException(string message) : base(message) { } }
Pattern Matching trong Handler
C#
// Trong GlobalExceptionHandler — clean pattern matching var statusCode = exception switch { AppException appEx => appEx.StatusCode, // Dùng StatusCode từ exception UnauthorizedAccessException => 401, OperationCanceledException => 499, // Client closed request _ => 500 }; // Thêm validation errors vào ProblemDetails nếu có var problemDetails = new ProblemDetails { Status = statusCode, Title = exception.GetType().Name, Detail = exception.Message, Instance = httpContext.Request.Path }; if (exception is ValidationException validationEx) { problemDetails.Extensions["errors"] = validationEx.Errors; }
Đặt
StatusCode property trên base AppException giúp handler không cần biết từng loại
exception cụ thể. Chỉ cần appEx.StatusCode là đủ — mở rộng thêm exception mới mà không sửa
handler.