444 lines
13 KiB
Markdown
444 lines
13 KiB
Markdown
---
|
|
name: error-handling-patterns
|
|
description: Global error handling, domain exceptions, và Result pattern. Use for exception middleware, validation errors, Polly resiliency, và health checks.
|
|
compatibility: ".NET 10+, Polly, FluentValidation, Microsoft.Extensions.Diagnostics.HealthChecks"
|
|
metadata:
|
|
author: Velik Ho
|
|
version: "1.0"
|
|
---
|
|
|
|
# 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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
// ❌ 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
|
|
|
|
```csharp
|
|
// ❌ 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
|
|
|
|
```csharp
|
|
// ❌ 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
|
|
|
|
```csharp
|
|
// ❌ BAD: Ignoring cancellation
|
|
await Task.Delay(5000);
|
|
|
|
// ✅ GOOD: Respect cancellation
|
|
await Task.Delay(5000, cancellationToken);
|
|
```
|
|
|
|
## Quick Reference / Tham Chiếu Nhanh
|
|
|
|
### Error Response Format
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```csharp
|
|
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](./references/REFERENCE.md) - Full code examples
|
|
- [API Design](../api-design/SKILL.md) - API error responses
|
|
- [Security](../security/SKILL.md) - Authentication errors
|
|
- [Testing Patterns](../testing-patterns/SKILL.md) - Testing error handling
|
|
- [Project Rules](../project-rules/SKILL.md) - Coding standards
|