13 KiB
13 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|
| error-handling-patterns | Global error handling, domain exceptions, và Result pattern. Use for exception middleware, validation errors, Polly resiliency, và health checks. | .NET 10+, Polly, FluentValidation, Microsoft.Extensions.Diagnostics.HealthChecks |
|
Error Handling Patterns / Mẫu Xử Lý Lỗi
Error handling và resiliency patterns cho GoodGo microservices.
When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Implementing global exception handling / Triển khai xử lý exception toàn cục
- Creating domain exceptions / Tạo domain exceptions
- Setting up retry policies with Polly / Cài đặt retry policies với Polly
- Implementing Circuit Breaker pattern / Triển khai Circuit Breaker
- Configuring health checks / Cấu hình health checks
- Handling validation errors / Xử lý validation errors
Core Concepts / Khái Niệm Cốt Lõi
Exception Hierarchy / Phân Cấp Exception
ApplicationException (Base)
├── DomainException # Business rule violations
│ ├── ValidationException # Input validation errors
│ ├── NotFoundException # Resource not found
│ └── ConflictException # Duplicate resource
├── InfrastructureException # External service failures
│ ├── DatabaseException # Database errors
│ └── ExternalServiceException # Third-party API errors
└── AuthorizationException # Permission errors
HTTP Status Code Mapping / Ánh Xạ Status Code
| Exception Type | HTTP Status | Use Case |
|---|---|---|
ValidationException |
400 | Invalid input data |
UnauthorizedAccessException |
401 | Missing/invalid token |
ForbiddenException |
403 | No permission |
NotFoundException |
404 | Resource doesn't exist |
ConflictException |
409 | Duplicate resource |
DomainException |
422 | Business rule violation |
Exception |
500 | Unexpected errors |
Resiliency Patterns / Mẫu Phục Hồi
| Pattern | Purpose | When to Use |
|---|---|---|
| Retry | Thử lại khi lỗi tạm thời | Network timeouts, transient DB errors |
| Circuit Breaker | Ngắt mạch khi service chết | External API calls |
| Timeout | Giới hạn thời gian chờ | Long-running operations |
| Bulkhead | Cô lập tài nguyên | Prevent cascade failures |
Key Patterns / Mẫu Chính
Global Exception Middleware
/// <summary>
/// EN: Global exception handler middleware.
/// VI: Middleware xử lý exception toàn cục.
/// </summary>
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlerMiddleware> _logger;
public ExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
// EN: Log full exception details
// VI: Log chi tiết exception đầy đủ
_logger.LogError(ex,
"EN: Unhandled exception / VI: Exception không xử lý: {Path}",
context.Request.Path);
var (statusCode, errorCode, message) = ex switch
{
ValidationException ve => (400, "VALIDATION_ERROR", ve.Message),
UnauthorizedAccessException => (401, "UNAUTHORIZED", "Authentication required"),
ForbiddenException => (403, "FORBIDDEN", "Access denied"),
NotFoundException nf => (404, "NOT_FOUND", nf.Message),
ConflictException ce => (409, "CONFLICT", ce.Message),
DomainException de => (422, "DOMAIN_ERROR", de.Message),
_ => (500, "INTERNAL_ERROR", "An unexpected error occurred")
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
var response = new ApiResponse<object>
{
Success = false,
Error = message,
ErrorCode = errorCode
};
await context.Response.WriteAsJsonAsync(response);
}
}
// EN: Register in Program.cs / VI: Đăng ký trong Program.cs
app.UseMiddleware<ExceptionHandlerMiddleware>();
Domain Exception Classes
/// <summary>
/// EN: Base domain exception.
/// VI: Domain exception cơ sở.
/// </summary>
public class DomainException : Exception
{
public string ErrorCode { get; }
public DomainException(string message, string errorCode = "DOMAIN_ERROR")
: base(message)
{
ErrorCode = errorCode;
}
}
/// <summary>
/// EN: Resource not found exception.
/// VI: Exception không tìm thấy resource.
/// </summary>
public class NotFoundException : DomainException
{
public NotFoundException(string resourceType, object resourceId)
: base($"{resourceType} with ID {resourceId} was not found", "NOT_FOUND")
{ }
public NotFoundException(string message)
: base(message, "NOT_FOUND")
{ }
}
/// <summary>
/// EN: Conflict/duplicate resource exception.
/// VI: Exception trùng lặp resource.
/// </summary>
public class ConflictException : DomainException
{
public ConflictException(string message)
: base(message, "CONFLICT")
{ }
}
/// <summary>
/// EN: Validation exception with errors.
/// VI: Exception validation với danh sách lỗi.
/// </summary>
public class ValidationException : DomainException
{
public IReadOnlyDictionary<string, string[]> Errors { get; }
public ValidationException(string message)
: base(message, "VALIDATION_ERROR")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(IReadOnlyDictionary<string, string[]> errors)
: base("One or more validation errors occurred", "VALIDATION_ERROR")
{
Errors = errors;
}
}
Retry with Polly
/// <summary>
/// EN: Configure Polly retry policies.
/// VI: Cấu hình Polly retry policies.
/// </summary>
// EN: Register resilient HttpClient / VI: Đăng ký HttpClient có khả năng phục hồi
builder.Services.AddHttpClient<IExternalService, ExternalService>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Services:External:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddResilienceHandler("external-api", builder =>
{
// EN: Retry with exponential backoff
// VI: Retry với thời gian chờ tăng dần
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => !r.IsSuccessStatusCode && (int)r.StatusCode >= 500)
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>(),
OnRetry = args =>
{
Console.WriteLine($"Retry {args.AttemptNumber} after {args.RetryDelay}");
return ValueTask.CompletedTask;
}
});
// EN: Circuit breaker
// VI: Circuit breaker
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
});
// EN: Timeout
// VI: Timeout
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
Health Checks
/// <summary>
/// EN: Configure health checks.
/// VI: Cấu hình health checks.
/// </summary>
// EN: Register health checks / VI: Đăng ký health checks
builder.Services.AddHealthChecks()
.AddNpgSql(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
name: "postgresql",
tags: new[] { "db", "critical" })
.AddRedis(
redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
name: "redis",
tags: new[] { "cache" })
.AddCheck<ExternalServiceHealthCheck>("external-service", tags: new[] { "external" });
// EN: Map health endpoints / VI: Map health endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // EN: Just check app is running / VI: Chỉ kiểm tra app đang chạy
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("critical")
});
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
Custom Health Check
/// <summary>
/// EN: Custom health check for external service.
/// VI: Health check tùy chỉnh cho external service.
/// </summary>
public class ExternalServiceHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
public ExternalServiceHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("ExternalService");
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken ct = default)
{
try
{
var response = await _httpClient.GetAsync("/health", ct);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy("External service is healthy")
: HealthCheckResult.Degraded($"External service returned {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"External service is unavailable",
exception: ex);
}
}
}
Common Mistakes / Lỗi Thường Gặp
1. Catching Generic Exceptions
// ❌ BAD: Swallowing all exceptions
try
{
await ProcessOrderAsync(order);
}
catch (Exception)
{
// Do nothing
}
// ✅ GOOD: Handle specific exceptions
try
{
await ProcessOrderAsync(order);
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation failed");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error processing order");
throw;
}
2. Exposing Internal Errors
// ❌ BAD: Exposing stack trace
return StatusCode(500, new { Error = ex.ToString() });
// ✅ GOOD: Generic message for internal errors
return StatusCode(500, new ApiResponse<object>
{
Success = false,
Error = "An unexpected error occurred",
ErrorCode = "INTERNAL_ERROR"
});
3. Missing Retry Configuration
// ❌ BAD: No retry for transient failures
var response = await _httpClient.GetAsync("/api/data");
// ✅ GOOD: Use resilient HttpClient with Polly
// (configured at DI registration level)
var response = await _resilientHttpClient.GetAsync("/api/data");
4. Ignoring Cancellation Token
// ❌ BAD: Ignoring cancellation
await Task.Delay(5000);
// ✅ GOOD: Respect cancellation
await Task.Delay(5000, cancellationToken);
Quick Reference / Tham Chiếu Nhanh
Error Response Format
{
"success": false,
"error": "User-friendly error message",
"errorCode": "VALIDATION_ERROR",
"errors": {
"email": ["Email is required", "Invalid email format"],
"password": ["Password must be at least 8 characters"]
}
}
Polly Strategies
| Strategy | Purpose | Typical Config |
|---|---|---|
| Retry | Retry transient failures | 3 attempts, exponential backoff |
| Circuit Breaker | Stop calling failing service | 50% failure, 30s break |
| Timeout | Limit wait time | 10-30 seconds |
| Rate Limiter | Throttle requests | X requests/minute |
Health Check Tags
| Tag | Purpose |
|---|---|
critical |
Required for app to function |
db |
Database connectivity |
cache |
Cache connectivity |
external |
External service |
Common Status Codes
return BadRequest(...); // 400
return Unauthorized(...); // 401
return Forbid(...); // 403
return NotFound(...); // 404
return Conflict(...); // 409
return UnprocessableEntity(...); // 422
return StatusCode(500, ...); // 500
Resources / Tài Nguyên
- Detailed Examples - Full code examples
- API Design - API error responses
- Security - Authentication errors
- Testing Patterns - Testing error handling
- Project Rules - Coding standards