Files
pos-system/microservices/.agent/skills/error-handling-patterns/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

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