Migrate
This commit is contained in:
@@ -0,0 +1,889 @@
|
||||
# Error Handling Patterns - Detailed Reference
|
||||
|
||||
Detailed code examples for error handling and resiliency patterns in ASP.NET Core.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Exception Middleware](#exception-middleware)
|
||||
2. [Domain Exceptions](#domain-exceptions)
|
||||
3. [Validation with FluentValidation](#validation-with-fluentvalidation)
|
||||
4. [Result Pattern](#result-pattern)
|
||||
5. [Polly Resiliency](#polly-resiliency)
|
||||
6. [Health Checks](#health-checks)
|
||||
7. [Problem Details](#problem-details)
|
||||
|
||||
---
|
||||
|
||||
## Exception Middleware
|
||||
|
||||
### Complete Exception Handler
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
- [Microsoft: Handle Errors in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/web-api/handle-errors)
|
||||
- [Polly Documentation](https://github.com/App-vNext/Polly)
|
||||
- [FluentValidation](https://docs.fluentvalidation.net/)
|
||||
- [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807)
|
||||
- [Health Checks in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks)
|
||||
Reference in New Issue
Block a user