Files
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

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
author version
Velik Ho 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

/// <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