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

26 KiB

Error Handling Patterns - Detailed Reference

Detailed code examples for error handling and resiliency patterns in ASP.NET Core.

Table of Contents

  1. Exception Middleware
  2. Domain Exceptions
  3. Validation with FluentValidation
  4. Result Pattern
  5. Polly Resiliency
  6. Health Checks
  7. Problem Details

Exception Middleware

Complete Exception Handler

/// <summary>
/// EN: Global exception handler middleware with detailed error mapping.
/// VI: Middleware xử lý exception toàn cục với ánh xạ lỗi chi tiết.
/// </summary>
public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlerMiddleware> _logger;
    private readonly IHostEnvironment _environment;

    public ExceptionHandlerMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlerMiddleware> logger,
        IHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    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)
    {
        var traceId = Activity.Current?.Id ?? context.TraceIdentifier;

        // EN: Log with appropriate level based on exception type
        // VI: Log với level phù hợp dựa trên loại exception
        LogException(ex, traceId);

        var response = CreateErrorResponse(ex, traceId);

        context.Response.StatusCode = response.StatusCode;
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsJsonAsync(response.Body);
    }

    private void LogException(Exception ex, string traceId)
    {
        var message = "EN: Error occurred / VI: Đã xảy ra lỗi TraceId: {TraceId}";

        switch (ex)
        {
            case ValidationException:
            case NotFoundException:
                _logger.LogWarning(ex, message, traceId);
                break;
            case DomainException:
                _logger.LogWarning(ex, message, traceId);
                break;
            default:
                _logger.LogError(ex, message, traceId);
                break;
        }
    }

    private (int StatusCode, object Body) CreateErrorResponse(Exception ex, string traceId)
    {
        return ex switch
        {
            ValidationException ve => (400, new ErrorResponse
            {
                Success = false,
                Error = ve.Message,
                ErrorCode = "VALIDATION_ERROR",
                Errors = ve.Errors,
                TraceId = traceId
            }),

            UnauthorizedAccessException => (401, new ErrorResponse
            {
                Success = false,
                Error = "Authentication required",
                ErrorCode = "UNAUTHORIZED",
                TraceId = traceId
            }),

            ForbiddenException fe => (403, new ErrorResponse
            {
                Success = false,
                Error = fe.Message,
                ErrorCode = "FORBIDDEN",
                TraceId = traceId
            }),

            NotFoundException nf => (404, new ErrorResponse
            {
                Success = false,
                Error = nf.Message,
                ErrorCode = "NOT_FOUND",
                TraceId = traceId
            }),

            ConflictException ce => (409, new ErrorResponse
            {
                Success = false,
                Error = ce.Message,
                ErrorCode = "CONFLICT",
                TraceId = traceId
            }),

            DomainException de => (422, new ErrorResponse
            {
                Success = false,
                Error = de.Message,
                ErrorCode = de.ErrorCode,
                TraceId = traceId
            }),

            _ => (500, new ErrorResponse
            {
                Success = false,
                Error = _environment.IsDevelopment()
                    ? ex.Message
                    : "An unexpected error occurred",
                ErrorCode = "INTERNAL_ERROR",
                TraceId = traceId,
                StackTrace = _environment.IsDevelopment() ? ex.StackTrace : null
            })
        };
    }
}

/// <summary>
/// EN: Standardized error response.
/// VI: Response lỗi chuẩn hóa.
/// </summary>
public class ErrorResponse
{
    public bool Success { get; set; }
    public string Error { get; set; } = string.Empty;
    public string ErrorCode { get; set; } = string.Empty;
    public string? TraceId { get; set; }
    public IReadOnlyDictionary<string, string[]>? Errors { get; set; }
    public string? StackTrace { get; set; }
}

Domain Exceptions

Complete Exception Hierarchy

/// <summary>
/// EN: Base domain exception with error code.
/// VI: Domain exception cơ sở với error code.
/// </summary>
public abstract class DomainException : Exception
{
    public string ErrorCode { get; }

    protected DomainException(string message, string errorCode)
        : base(message)
    {
        ErrorCode = errorCode;
    }

    protected DomainException(string message, string errorCode, Exception innerException)
        : base(message, innerException)
    {
        ErrorCode = errorCode;
    }
}

/// <summary>
/// EN: Validation exception with field-level errors.
/// VI: Exception validation với lỗi từng field.
/// </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(string field, string error)
        : base($"Validation failed for {field}", "VALIDATION_ERROR")
    {
        Errors = new Dictionary<string, string[]>
        {
            { field, new[] { error } }
        };
    }

    public ValidationException(IDictionary<string, string[]> errors)
        : base("One or more validation errors occurred", "VALIDATION_ERROR")
    {
        Errors = new Dictionary<string, string[]>(errors);
    }

    public ValidationException(IEnumerable<FluentValidation.Results.ValidationFailure> failures)
        : base("One or more validation errors occurred", "VALIDATION_ERROR")
    {
        Errors = failures
            .GroupBy(f => f.PropertyName)
            .ToDictionary(
                g => g.Key,
                g => g.Select(f => f.ErrorMessage).ToArray());
    }
}

/// <summary>
/// EN: Resource not found exception.
/// VI: Exception không tìm thấy resource.
/// </summary>
public class NotFoundException : DomainException
{
    public string ResourceType { get; }
    public object? ResourceId { get; }

    public NotFoundException(string resourceType, object resourceId)
        : base($"{resourceType} with ID '{resourceId}' was not found", "NOT_FOUND")
    {
        ResourceType = resourceType;
        ResourceId = resourceId;
    }

    public NotFoundException(string message)
        : base(message, "NOT_FOUND")
    {
        ResourceType = "Resource";
    }
}

/// <summary>
/// EN: Conflict exception for duplicate resources.
/// VI: Exception xung đột cho resource trùng lặp.
/// </summary>
public class ConflictException : DomainException
{
    public ConflictException(string message)
        : base(message, "CONFLICT")
    { }

    public ConflictException(string resourceType, string identifier)
        : base($"{resourceType} with identifier '{identifier}' already exists", "CONFLICT")
    { }
}

/// <summary>
/// EN: Forbidden exception for permission errors.
/// VI: Exception forbidden cho lỗi phân quyền.
/// </summary>
public class ForbiddenException : DomainException
{
    public ForbiddenException(string message = "You do not have permission to perform this action")
        : base(message, "FORBIDDEN")
    { }
}

/// <summary>
/// EN: Business rule exception.
/// VI: Exception quy tắc nghiệp vụ.
/// </summary>
public class BusinessRuleException : DomainException
{
    public BusinessRuleException(string message, string errorCode = "BUSINESS_RULE_VIOLATION")
        : base(message, errorCode)
    { }
}

Validation with FluentValidation

Validation Behavior for MediatR

/// <summary>
/// EN: MediatR pipeline behavior for FluentValidation.
/// VI: MediatR pipeline behavior cho FluentValidation.
/// </summary>
public class ValidationBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
            throw new ValidationException(failures);

        return await next();
    }
}

// EN: Registration in Program.cs / VI: Đăng ký trong Program.cs
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});

builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

Sample Validators

/// <summary>
/// EN: Validator for CreateOrderCommand.
/// VI: Validator cho CreateOrderCommand.
/// </summary>
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.UserId)
            .NotEmpty().WithMessage("User ID is required");

        RuleFor(x => x.ShippingAddress)
            .NotNull().WithMessage("Shipping address is required")
            .SetValidator(new AddressValidator()!);

        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("At least one item is required");

        RuleForEach(x => x.Items)
            .SetValidator(new OrderItemValidator());
    }
}

public class AddressValidator : AbstractValidator<Address>
{
    public AddressValidator()
    {
        RuleFor(x => x.Street)
            .NotEmpty().WithMessage("Street is required")
            .MaximumLength(200);

        RuleFor(x => x.City)
            .NotEmpty().WithMessage("City is required")
            .MaximumLength(100);

        RuleFor(x => x.PostalCode)
            .NotEmpty().WithMessage("Postal code is required")
            .Matches(@"^\d{5}(-\d{4})?$").WithMessage("Invalid postal code format");
    }
}

public class OrderItemValidator : AbstractValidator<OrderItemDto>
{
    public OrderItemValidator()
    {
        RuleFor(x => x.ProductId)
            .NotEmpty().WithMessage("Product ID is required");

        RuleFor(x => x.Quantity)
            .GreaterThan(0).WithMessage("Quantity must be positive");

        RuleFor(x => x.UnitPrice)
            .GreaterThanOrEqualTo(0).WithMessage("Price cannot be negative");
    }
}

Result Pattern

Generic Result Type

/// <summary>
/// EN: Result type for operations that can fail.
/// VI: Result type cho operations có thể thất bại.
/// </summary>
public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public string? Error { get; }
    public string? ErrorCode { get; }

    protected Result(bool isSuccess, string? error, string? errorCode)
    {
        IsSuccess = isSuccess;
        Error = error;
        ErrorCode = errorCode;
    }

    public static Result Success() => new(true, null, null);
    public static Result Failure(string error, string errorCode = "ERROR") 
        => new(false, error, errorCode);
    public static Result<T> Success<T>(T value) => new(value, true, null, null);
    public static Result<T> Failure<T>(string error, string errorCode = "ERROR") 
        => new(default!, false, error, errorCode);
}

/// <summary>
/// EN: Result type with value.
/// VI: Result type có giá trị.
/// </summary>
public class Result<T> : Result
{
    public T Value { get; }

    internal Result(T value, bool isSuccess, string? error, string? errorCode)
        : base(isSuccess, error, errorCode)
    {
        Value = value;
    }

    public static implicit operator Result<T>(T value) => Success(value);
}

/// <summary>
/// EN: Result extensions for common patterns.
/// VI: Extensions cho các mẫu phổ biến.
/// </summary>
public static class ResultExtensions
{
    public static Result<TOut> Map<TIn, TOut>(
        this Result<TIn> result, 
        Func<TIn, TOut> mapper)
    {
        return result.IsSuccess
            ? Result.Success(mapper(result.Value))
            : Result.Failure<TOut>(result.Error!, result.ErrorCode!);
    }

    public static async Task<Result<TOut>> MapAsync<TIn, TOut>(
        this Task<Result<TIn>> resultTask,
        Func<TIn, TOut> mapper)
    {
        var result = await resultTask;
        return result.Map(mapper);
    }

    public static Result<T> Ensure<T>(
        this Result<T> result,
        Func<T, bool> predicate,
        string error,
        string errorCode = "ERROR")
    {
        if (result.IsFailure)
            return result;

        return predicate(result.Value)
            ? result
            : Result.Failure<T>(error, errorCode);
    }
}

Using Result Pattern

/// <summary>
/// EN: Command handler returning Result instead of throwing.
/// VI: Command handler trả về Result thay vì throw.
/// </summary>
public class CreateOrderCommandHandler 
    : IRequestHandler<CreateOrderCommand, Result<OrderResult>>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUserService _userService;

    public async Task<Result<OrderResult>> Handle(
        CreateOrderCommand request,
        CancellationToken ct)
    {
        // EN: Validate user exists
        // VI: Xác thực user tồn tại
        var userExists = await _userService.ExistsAsync(request.UserId, ct);
        if (!userExists)
            return Result.Failure<OrderResult>("User not found", "USER_NOT_FOUND");

        // EN: Validate items
        // VI: Xác thực items
        if (!request.Items.Any())
            return Result.Failure<OrderResult>("At least one item required", "EMPTY_ORDER");

        // EN: Create order
        // VI: Tạo order
        var order = new Order(request.UserId, request.ShippingAddress);
        foreach (var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
        }

        await _orderRepository.AddAsync(order, ct);
        await _orderRepository.UnitOfWork.SaveChangesAsync(ct);

        return Result.Success(new OrderResult(order.Id));
    }
}

// EN: Controller using Result pattern / VI: Controller dùng Result pattern
[HttpPost]
public async Task<IActionResult> CreateOrder(
    CreateOrderRequest request,
    CancellationToken ct)
{
    var command = request.ToCommand(GetUserId());
    var result = await _mediator.Send(command, ct);

    if (result.IsFailure)
    {
        return result.ErrorCode switch
        {
            "USER_NOT_FOUND" => NotFound(ApiResponse.Fail(result.Error!, result.ErrorCode)),
            "EMPTY_ORDER" => BadRequest(ApiResponse.Fail(result.Error!, result.ErrorCode)),
            _ => StatusCode(500, ApiResponse.Fail(result.Error!, result.ErrorCode))
        };
    }

    return CreatedAtAction(
        nameof(GetOrder),
        new { orderId = result.Value.OrderId },
        ApiResponse.Ok(result.Value));
}

Polly Resiliency

Comprehensive Resilience Configuration

/// <summary>
/// EN: Configure resilient HTTP client with Polly.
/// VI: Cấu hình HTTP client có khả năng phục hồi với Polly.
/// </summary>

// EN: Using Microsoft.Extensions.Http.Resilience (Polly v8 integration)
// VI: Sử dụng Microsoft.Extensions.Http.Resilience (tích hợp Polly v8)
builder.Services.AddHttpClient<IExternalPaymentService, ExternalPaymentService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:Payment:BaseUrl"]!);
})
.AddResilienceHandler("payment-service", builder =>
{
    // EN: Retry policy with exponential backoff
    // VI: Retry policy với thời gian chờ tăng dần
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(500),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true, // EN: Add randomness / VI: Thêm ngẫu nhiên
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .HandleResult(r => (int)r.StatusCode >= 500)
            .Handle<HttpRequestException>()
            .Handle<TimeoutRejectedException>(),
        OnRetry = args =>
        {
            Console.WriteLine(
                $"EN: Retry {args.AttemptNumber} after {args.RetryDelay} / " +
                $"VI: Thử lại lần {args.AttemptNumber} sau {args.RetryDelay}");
            return ValueTask.CompletedTask;
        }
    });

    // EN: Circuit breaker
    // VI: Circuit breaker
    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5, // EN: 50% failures / VI: 50% thất bại
        SamplingDuration = TimeSpan.FromSeconds(30),
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(30),
        OnOpened = args =>
        {
            Console.WriteLine("EN: Circuit opened / VI: Circuit đã mở");
            return ValueTask.CompletedTask;
        },
        OnClosed = args =>
        {
            Console.WriteLine("EN: Circuit closed / VI: Circuit đã đóng");
            return ValueTask.CompletedTask;
        }
    });

    // EN: Timeout per request
    // VI: Timeout cho mỗi request
    builder.AddTimeout(TimeSpan.FromSeconds(10));
});

Resilient Database Operations

/// <summary>
/// EN: Configure EF Core with retry logic.
/// VI: Cấu hình EF Core với logic retry.
/// </summary>
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        npgsqlOptions =>
        {
            // EN: Enable retry on transient failures
            // VI: Bật retry cho lỗi tạm thời
            npgsqlOptions.EnableRetryOnFailure(
                maxRetryCount: 3,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorCodesToAdd: null);
            
            npgsqlOptions.CommandTimeout(30);
        });
});

Health Checks

Comprehensive Health Check Setup

/// <summary>
/// EN: Configure comprehensive health checks.
/// VI: Cấu hình health checks toàn diện.
/// </summary>

// EN: Add health checks / VI: Thêm health checks
builder.Services.AddHealthChecks()
    // EN: Database / VI: Database
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "postgresql",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "critical" })
    
    // EN: Redis cache / VI: Redis cache
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
        name: "redis",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "cache" })
    
    // EN: External services / VI: Services bên ngoài
    .AddUrlGroup(
        new Uri(builder.Configuration["Services:Payment:HealthUrl"]!),
        name: "payment-service",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "external" })
    
    // EN: Custom health check / VI: Health check tùy chỉnh
    .AddCheck<StorageHealthCheck>("storage", tags: new[] { "storage", "critical" });

// EN: Configure endpoints / VI: Cấu hình endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    // EN: Liveness - just check if app is running
    // VI: Liveness - chỉ kiểm tra app có đang chạy không
    Predicate = _ => false,
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync("{\"status\":\"Healthy\"}");
    }
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    // EN: Readiness - check critical dependencies
    // VI: Readiness - kiểm tra các dependency quan trọng
    Predicate = check => check.Tags.Contains("critical"),
    ResponseWriter = WriteHealthCheckResponse
});

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = WriteHealthCheckResponse
});

static async Task WriteHealthCheckResponse(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "application/json";

    var result = new
    {
        status = report.Status.ToString(),
        totalDuration = report.TotalDuration.TotalMilliseconds,
        checks = report.Entries.Select(e => new
        {
            name = e.Key,
            status = e.Value.Status.ToString(),
            duration = e.Value.Duration.TotalMilliseconds,
            description = e.Value.Description,
            exception = e.Value.Exception?.Message
        })
    };

    await context.Response.WriteAsJsonAsync(result);
}

Custom Health Check Implementation

/// <summary>
/// EN: Custom health check for storage service.
/// VI: Health check tùy chỉnh cho storage service.
/// </summary>
public class StorageHealthCheck : IHealthCheck
{
    private readonly IMinioClient _minioClient;
    private readonly ILogger<StorageHealthCheck> _logger;

    public StorageHealthCheck(
        IMinioClient minioClient,
        ILogger<StorageHealthCheck> logger)
    {
        _minioClient = minioClient;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // EN: Check if bucket exists and is accessible
            // VI: Kiểm tra bucket tồn tại và có thể truy cập
            var bucketExists = await _minioClient.BucketExistsAsync(
                new BucketExistsArgs().WithBucket("uploads"),
                cancellationToken);

            if (!bucketExists)
            {
                return HealthCheckResult.Degraded(
                    "Storage bucket not found",
                    data: new Dictionary<string, object>
                    {
                        { "bucket", "uploads" }
                    });
            }

            return HealthCheckResult.Healthy(
                "Storage service is healthy",
                data: new Dictionary<string, object>
                {
                    { "bucket", "uploads" },
                    { "status", "available" }
                });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "EN: Storage health check failed / VI: Health check storage thất bại");
            
            return HealthCheckResult.Unhealthy(
                "Storage service is unavailable",
                exception: ex);
        }
    }
}

Problem Details

RFC 7807 Problem Details

/// <summary>
/// EN: Configure Problem Details for standardized errors.
/// VI: Cấu hình Problem Details cho lỗi chuẩn hóa.
/// </summary>

// EN: Add Problem Details / VI: Thêm Problem Details
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] = 
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
        context.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
    };
});

// EN: Use exception handler with Problem Details
// VI: Sử dụng exception handler với Problem Details
app.UseExceptionHandler(appBuilder =>
{
    appBuilder.Run(async context =>
    {
        var exceptionHandler = context.Features.Get<IExceptionHandlerFeature>();
        var exception = exceptionHandler?.Error;

        if (exception != null)
        {
            var problemDetails = exception switch
            {
                ValidationException ve => new ProblemDetails
                {
                    Status = 400,
                    Title = "Validation Error",
                    Detail = ve.Message,
                    Extensions = { ["errors"] = ve.Errors }
                },
                NotFoundException nf => new ProblemDetails
                {
                    Status = 404,
                    Title = "Not Found",
                    Detail = nf.Message
                },
                _ => new ProblemDetails
                {
                    Status = 500,
                    Title = "Internal Server Error",
                    Detail = "An unexpected error occurred"
                }
            };

            problemDetails.Extensions["traceId"] = context.TraceIdentifier;
            context.Response.StatusCode = problemDetails.Status ?? 500;
            context.Response.ContentType = "application/problem+json";
            await context.Response.WriteAsJsonAsync(problemDetails);
        }
    });
});

Resources / Tài Nguyên