diff --git a/.agent/skills/api-design/SKILL.md b/.agent/skills/api-design/SKILL.md
index 3b70d822..e92529db 100644
--- a/.agent/skills/api-design/SKILL.md
+++ b/.agent/skills/api-design/SKILL.md
@@ -1,7 +1,7 @@
---
name: api-design
description: RESTful API design standards for GoodGo microservices. Use for new API endpoints, DTOs, controllers, OpenAPI documentation, or standardized responses.
-compatibility: ".NET 8+, ASP.NET Core, MediatR, Swashbuckle, Asp.Versioning"
+compatibility: ".NET 10+, ASP.NET Core, MediatR, Swashbuckle, Asp.Versioning"
metadata:
author: Velik Ho
version: "2.0"
diff --git a/.agent/skills/cqrs-mediatr/SKILL.md b/.agent/skills/cqrs-mediatr/SKILL.md
new file mode 100644
index 00000000..2280f8ee
--- /dev/null
+++ b/.agent/skills/cqrs-mediatr/SKILL.md
@@ -0,0 +1,454 @@
+---
+name: cqrs-mediatr
+description: CQRS pattern với MediatR. Use for Commands, Queries, Handlers, Pipeline Behaviors, và Idempotency.
+compatibility: ".NET 10+, MediatR 12+, FluentValidation"
+metadata:
+ author: Velik Ho
+ version: "1.0"
+---
+
+# CQRS & MediatR Patterns / Mẫu CQRS & MediatR
+
+CQRS pattern với MediatR cho GoodGo microservices.
+
+## When to Use This Skill / Khi Nào Sử Dụng
+
+Use this skill when:
+- Separating read/write operations / Tách biệt operations đọc/ghi
+- Creating Commands and Queries / Tạo Commands và Queries
+- Implementing MediatR handlers / Triển khai MediatR handlers
+- Adding cross-cutting concerns via Behaviors / Thêm behaviors xuyên suốt
+- Ensuring idempotency / Đảm bảo tính idempotent
+
+## Core Concepts / Khái Niệm Cốt Lõi
+
+### CQRS Overview / Tổng Quan CQRS
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ API Controller │
+└─────────────────────┬───────────────────────────┬───────────┘
+ │ │
+ ┌───────▼───────┐ ┌───────▼───────┐
+ │ Commands │ │ Queries │
+ │ (Write/Ghi) │ │ (Read/Đọc) │
+ └───────┬───────┘ └───────┬───────┘
+ │ │
+ ┌───────▼───────┐ ┌───────▼───────┐
+ │ MediatR │ │ MediatR │
+ │ Handler │ │ Handler │
+ └───────┬───────┘ └───────┬───────┘
+ │ │
+ ┌───────▼───────┐ ┌───────▼───────┐
+ │ Domain Model │ │ Dapper │
+ │ EF Core │ │ Raw SQL │
+ └───────┬───────┘ └───────┬───────┘
+ │ │
+ └───────────────┬───────────────────┘
+ │
+ ┌────────▼────────┐
+ │ Database │
+ └─────────────────┘
+```
+
+### Command vs Query / Lệnh vs Truy Vấn
+
+| Aspect | Command | Query |
+|--------|---------|-------|
+| **Purpose** | Modify state | Read data |
+| **Returns** | Result/void | Data/DTO |
+| **Model** | Full Domain Model | Lightweight DTO |
+| **ORM** | EF Core | Dapper/Raw SQL |
+| **Validation** | Full business rules | Minimal |
+
+### MediatR Flow / Luồng MediatR
+
+```
+Request → Pipeline Behaviors → Handler → Response
+ ├── LoggingBehavior
+ ├── ValidationBehavior
+ └── TransactionBehavior
+```
+
+## Key Patterns / Mẫu Chính
+
+### Command Definition / Định Nghĩa Command
+
+```csharp
+///
+/// EN: Command to create a new order.
+/// VI: Command tạo order mới.
+///
+public record CreateOrderCommand(
+ string UserId,
+ Address ShippingAddress,
+ List Items) : IRequest;
+
+///
+/// EN: Command result.
+/// VI: Kết quả command.
+///
+public record OrderResult(Guid OrderId);
+```
+
+### Query Definition / Định Nghĩa Query
+
+```csharp
+///
+/// EN: Query to get user orders.
+/// VI: Query lấy orders của user.
+///
+public record GetUserOrdersQuery(
+ string UserId,
+ int Skip = 0,
+ int Take = 20) : IRequest>;
+
+///
+/// EN: Lightweight DTO for query results.
+/// VI: DTO nhẹ cho kết quả query.
+///
+public record OrderSummaryDto(
+ Guid Id,
+ string Status,
+ decimal TotalAmount,
+ DateTime CreatedAt,
+ int ItemCount);
+```
+
+### Command Handler / Handler Command
+
+```csharp
+///
+/// EN: Handler for CreateOrderCommand.
+/// VI: Handler cho CreateOrderCommand.
+///
+public class CreateOrderCommandHandler
+ : IRequestHandler
+{
+ private readonly IOrderRepository _orderRepository;
+ private readonly ILogger _logger;
+
+ public CreateOrderCommandHandler(
+ IOrderRepository orderRepository,
+ ILogger logger)
+ {
+ _orderRepository = orderRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ CreateOrderCommand request,
+ CancellationToken ct)
+ {
+ // EN: Create order through domain model
+ // VI: Tạo order qua domain model
+ 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);
+
+ _logger.LogInformation(
+ "EN: Order created / VI: Order đã tạo: {OrderId}",
+ order.Id);
+
+ return new OrderResult(order.Id);
+ }
+}
+```
+
+### Query Handler with Dapper / Handler Query với Dapper
+
+```csharp
+///
+/// EN: Query handler using Dapper for optimized reads.
+/// VI: Query handler dùng Dapper cho đọc tối ưu.
+///
+public class GetUserOrdersQueryHandler
+ : IRequestHandler>
+{
+ private readonly IDbConnection _connection;
+
+ public GetUserOrdersQueryHandler(IDbConnection connection)
+ {
+ _connection = connection;
+ }
+
+ public async Task> Handle(
+ GetUserOrdersQuery request,
+ CancellationToken ct)
+ {
+ const string countSql = "SELECT COUNT(*) FROM Orders WHERE UserId = @UserId";
+ var total = await _connection.ExecuteScalarAsync(countSql, new { request.UserId });
+
+ const string sql = @"
+ SELECT o.Id, o.Status, o.TotalAmount, o.CreatedAt,
+ COUNT(oi.Id) as ItemCount
+ FROM Orders o
+ LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
+ WHERE o.UserId = @UserId
+ GROUP BY o.Id, o.Status, o.TotalAmount, o.CreatedAt
+ ORDER BY o.CreatedAt DESC
+ OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY";
+
+ var orders = await _connection.QueryAsync(sql, new
+ {
+ request.UserId,
+ request.Skip,
+ request.Take
+ });
+
+ return new PagedResult(orders.ToList(), total);
+ }
+}
+```
+
+### Validation Behavior / Behavior Validation
+
+```csharp
+///
+/// EN: Pipeline behavior for FluentValidation.
+/// VI: Pipeline behavior cho FluentValidation.
+///
+public class ValidationBehavior
+ : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly IEnumerable> _validators;
+
+ public ValidationBehavior(IEnumerable> validators)
+ {
+ _validators = validators;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken ct)
+ {
+ if (!_validators.Any())
+ return await next();
+
+ var context = new ValidationContext(request);
+ var failures = (await Task.WhenAll(
+ _validators.Select(v => v.ValidateAsync(context, ct))))
+ .SelectMany(r => r.Errors)
+ .Where(f => f != null)
+ .ToList();
+
+ if (failures.Count != 0)
+ throw new ValidationException(failures);
+
+ return await next();
+ }
+}
+```
+
+### Logging Behavior / Behavior Logging
+
+```csharp
+///
+/// EN: Pipeline behavior for logging.
+/// VI: Pipeline behavior cho logging.
+///
+public class LoggingBehavior
+ : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly ILogger> _logger;
+
+ public LoggingBehavior(ILogger> logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken ct)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ _logger.LogInformation(
+ "EN: Handling {RequestName} / VI: Xử lý {RequestName}",
+ requestName);
+
+ var stopwatch = Stopwatch.StartNew();
+ var response = await next();
+ stopwatch.Stop();
+
+ _logger.LogInformation(
+ "EN: Handled {RequestName} in {ElapsedMs}ms / VI: Đã xử lý {RequestName} trong {ElapsedMs}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ return response;
+ }
+}
+```
+
+### Controller with MediatR / Controller với MediatR
+
+```csharp
+///
+/// EN: Slim controller using MediatR.
+/// VI: Controller gọn nhẹ với MediatR.
+///
+[ApiController]
+[Route("api/v{version:apiVersion}/orders")]
+public class OrdersController : ControllerBase
+{
+ private readonly IMediator _mediator;
+
+ public OrdersController(IMediator mediator)
+ {
+ _mediator = mediator;
+ }
+
+ [HttpPost]
+ public async Task>> CreateOrder(
+ CreateOrderRequest request,
+ CancellationToken ct)
+ {
+ var command = new CreateOrderCommand(
+ GetUserId(),
+ request.ShippingAddress,
+ request.Items);
+
+ var result = await _mediator.Send(command, ct);
+
+ return CreatedAtAction(
+ nameof(GetOrder),
+ new { orderId = result.OrderId },
+ ApiResponse.Ok(result));
+ }
+
+ [HttpGet]
+ public async Task>>> GetOrders(
+ [FromQuery] int skip = 0,
+ [FromQuery] int take = 20,
+ CancellationToken ct = default)
+ {
+ var query = new GetUserOrdersQuery(GetUserId(), skip, take);
+ var result = await _mediator.Send(query, ct);
+ return Ok(ApiResponse>.Ok(result));
+ }
+
+ private string GetUserId() =>
+ User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new UnauthorizedAccessException();
+}
+```
+
+## Common Mistakes / Lỗi Thường Gặp
+
+### 1. Using Domain Model for Queries
+
+```csharp
+// ❌ BAD: Using EF Core for simple reads
+public async Task> Handle(GetOrdersQuery query, CancellationToken ct)
+{
+ return await _context.Orders
+ .Include(o => o.OrderItems)
+ .Where(o => o.UserId == query.UserId)
+ .ToListAsync(ct);
+}
+
+// ✅ GOOD: Using Dapper for optimized reads
+public async Task> Handle(GetOrdersQuery query, CancellationToken ct)
+{
+ const string sql = "SELECT Id, Status, TotalAmount FROM Orders WHERE UserId = @UserId";
+ return await _connection.QueryAsync(sql, new { query.UserId });
+}
+```
+
+### 2. Missing Pipeline Behaviors
+
+```csharp
+// ❌ BAD: Validation in handler
+public async Task Handle(CreateOrderCommand request, CancellationToken ct)
+{
+ if (string.IsNullOrEmpty(request.UserId))
+ throw new ValidationException("UserId required");
+ // ...
+}
+
+// ✅ GOOD: Validation via Behavior
+builder.Services.AddMediatR(cfg =>
+{
+ cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+});
+```
+
+### 3. Fat Controllers
+
+```csharp
+// ❌ BAD: Logic in controller
+[HttpPost]
+public async Task CreateOrder(CreateOrderRequest request)
+{
+ var order = new Order(request.UserId, request.Address);
+ foreach (var item in request.Items)
+ order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
+ await _repository.AddAsync(order);
+ await _repository.UnitOfWork.SaveChangesAsync();
+ return Ok(order.Id);
+}
+
+// ✅ GOOD: Delegate to MediatR
+[HttpPost]
+public async Task CreateOrder(CreateOrderRequest request)
+{
+ var command = request.ToCommand(GetUserId());
+ var result = await _mediator.Send(command);
+ return CreatedAtAction(nameof(GetOrder), new { orderId = result.OrderId }, result);
+}
+```
+
+## Quick Reference / Tham Chiếu Nhanh
+
+### MediatR Request Types
+
+| Interface | Purpose | Example |
+|-----------|---------|---------|
+| `IRequest` | Request with response | Commands, Queries |
+| `IRequest` | Request without response | Fire-and-forget |
+| `INotification` | Event notification | Domain events |
+
+### Pipeline Behavior Order
+
+```csharp
+// EN: Registration order = execution order
+// VI: Thứ tự đăng ký = thứ tự thực thi
+cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); // 1st
+cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // 2nd
+cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); // 3rd
+```
+
+### DI Registration
+
+```csharp
+// EN: Register MediatR with behaviors
+// VI: Đăng ký MediatR với behaviors
+builder.Services.AddMediatR(cfg =>
+{
+ cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+});
+
+// EN: Register FluentValidation
+// VI: Đăng ký FluentValidation
+builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
+```
+
+## Resources / Tài Nguyên
+
+- [Detailed Examples](./references/REFERENCE.md) - Full code examples
+- [Repository Pattern](../repository-pattern/SKILL.md) - Data access
+- [Error Handling](../error-handling-patterns/SKILL.md) - Validation errors
+- [Testing Patterns](../testing-patterns/SKILL.md) - Handler testing
+- [API Design](../api-design/SKILL.md) - Controller patterns
diff --git a/.agent/skills/cqrs-mediatr/references/REFERENCE.md b/.agent/skills/cqrs-mediatr/references/REFERENCE.md
new file mode 100644
index 00000000..be937a39
--- /dev/null
+++ b/.agent/skills/cqrs-mediatr/references/REFERENCE.md
@@ -0,0 +1,764 @@
+# CQRS-MediatR - Detailed Reference
+
+Detailed code examples for CQRS pattern với MediatR trong ASP.NET Core.
+
+## Table of Contents
+
+1. [Project Setup](#project-setup)
+2. [Command Patterns](#command-patterns)
+3. [Query Patterns](#query-patterns)
+4. [Pipeline Behaviors](#pipeline-behaviors)
+5. [Idempotency](#idempotency)
+6. [Domain Events](#domain-events)
+7. [Transaction Management](#transaction-management)
+
+---
+
+## Project Setup
+
+### Package Dependencies
+
+```xml
+
+
+
+
+
+
+```
+
+### DI Registration
+
+```csharp
+///
+/// EN: Configure MediatR with all behaviors.
+/// VI: Cấu hình MediatR với tất cả behaviors.
+///
+
+// EN: Register MediatR / VI: Đăng ký MediatR
+builder.Services.AddMediatR(cfg =>
+{
+ cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
+
+ // EN: Pipeline behaviors (order matters!)
+ // VI: Pipeline behaviors (thứ tự quan trọng!)
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
+});
+
+// EN: Register FluentValidation validators
+// VI: Đăng ký FluentValidation validators
+builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
+
+// EN: Register Dapper connection for queries
+// VI: Đăng ký Dapper connection cho queries
+builder.Services.AddScoped(_ =>
+ new NpgsqlConnection(builder.Configuration.GetConnectionString("DefaultConnection")));
+```
+
+---
+
+## Command Patterns
+
+### Immutable Command Records
+
+```csharp
+///
+/// EN: Command to create an order. Commands should be immutable.
+/// VI: Command tạo order. Commands nên là immutable.
+///
+public record CreateOrderCommand : IRequest
+{
+ public string UserId { get; init; }
+ public Address ShippingAddress { get; init; }
+ public IReadOnlyList Items { get; init; }
+
+ public CreateOrderCommand(
+ string userId,
+ Address shippingAddress,
+ IReadOnlyList items)
+ {
+ UserId = userId;
+ ShippingAddress = shippingAddress;
+ Items = items;
+ }
+}
+
+///
+/// EN: Command to update order status.
+/// VI: Command cập nhật trạng thái order.
+///
+public record UpdateOrderStatusCommand(
+ Guid OrderId,
+ string UserId,
+ OrderStatus NewStatus) : IRequest;
+
+///
+/// EN: Command to cancel order.
+/// VI: Command hủy order.
+///
+public record CancelOrderCommand(
+ Guid OrderId,
+ string UserId,
+ string Reason) : IRequest;
+```
+
+### Command Handler with Full Domain Logic
+
+```csharp
+///
+/// EN: Handler for CreateOrderCommand with domain validation.
+/// VI: Handler cho CreateOrderCommand với domain validation.
+///
+public class CreateOrderCommandHandler : IRequestHandler
+{
+ private readonly IOrderRepository _orderRepository;
+ private readonly IProductService _productService;
+ private readonly ILogger _logger;
+
+ public CreateOrderCommandHandler(
+ IOrderRepository orderRepository,
+ IProductService productService,
+ ILogger logger)
+ {
+ _orderRepository = orderRepository;
+ _productService = productService;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ CreateOrderCommand request,
+ CancellationToken ct)
+ {
+ // EN: Validate products exist and get prices
+ // VI: Xác thực products tồn tại và lấy giá
+ var productIds = request.Items.Select(i => i.ProductId).ToList();
+ var products = await _productService.GetByIdsAsync(productIds, ct);
+
+ if (products.Count != productIds.Count)
+ {
+ var missingIds = productIds.Except(products.Select(p => p.Id));
+ throw new NotFoundException("Products", string.Join(", ", missingIds));
+ }
+
+ // EN: Create order aggregate
+ // VI: Tạo order aggregate
+ var order = new Order(request.UserId, request.ShippingAddress);
+
+ foreach (var item in request.Items)
+ {
+ var product = products.First(p => p.Id == item.ProductId);
+ order.AddItem(product.Id, item.Quantity, product.Price);
+ }
+
+ // EN: Persist via repository
+ // VI: Lưu qua repository
+ await _orderRepository.AddAsync(order, ct);
+ await _orderRepository.UnitOfWork.SaveChangesAsync(ct);
+
+ _logger.LogInformation(
+ "EN: Order {OrderId} created for user {UserId} / " +
+ "VI: Order {OrderId} đã tạo cho user {UserId}",
+ order.Id, request.UserId);
+
+ return new OrderResult(order.Id, order.Status.ToString(), order.TotalAmount);
+ }
+}
+```
+
+### Command Validator
+
+```csharp
+///
+/// EN: FluentValidation validator for CreateOrderCommand.
+/// VI: FluentValidation validator cho CreateOrderCommand.
+///
+public class CreateOrderCommandValidator : AbstractValidator
+{
+ 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")
+ .Must(items => items.Count <= 100)
+ .WithMessage("Maximum 100 items per order");
+
+ RuleForEach(x => x.Items)
+ .SetValidator(new OrderItemDtoValidator());
+ }
+}
+
+public class AddressValidator : AbstractValidator
+{
+ public AddressValidator()
+ {
+ RuleFor(x => x.Street).NotEmpty().MaximumLength(200);
+ RuleFor(x => x.City).NotEmpty().MaximumLength(100);
+ RuleFor(x => x.PostalCode).NotEmpty().Matches(@"^\d{5}(-\d{4})?$");
+ RuleFor(x => x.Country).NotEmpty().MaximumLength(100);
+ }
+}
+
+public class OrderItemDtoValidator : AbstractValidator
+{
+ public OrderItemDtoValidator()
+ {
+ RuleFor(x => x.ProductId).NotEmpty();
+ RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(1000);
+ RuleFor(x => x.UnitPrice).GreaterThanOrEqualTo(0);
+ }
+}
+```
+
+---
+
+## Query Patterns
+
+### Optimized Query with Dapper
+
+```csharp
+///
+/// EN: Query for paginated user orders.
+/// VI: Query cho orders phân trang của user.
+///
+public record GetUserOrdersQuery(
+ string UserId,
+ int Skip = 0,
+ int Take = 20,
+ OrderStatus? StatusFilter = null) : IRequest>;
+
+///
+/// EN: Handler using Dapper for optimized reads.
+/// VI: Handler dùng Dapper cho đọc tối ưu.
+///
+public class GetUserOrdersQueryHandler
+ : IRequestHandler>
+{
+ private readonly IDbConnection _connection;
+
+ public GetUserOrdersQueryHandler(IDbConnection connection)
+ {
+ _connection = connection;
+ }
+
+ public async Task> Handle(
+ GetUserOrdersQuery request,
+ CancellationToken ct)
+ {
+ var whereClause = "WHERE o.UserId = @UserId";
+ var parameters = new DynamicParameters();
+ parameters.Add("UserId", request.UserId);
+ parameters.Add("Skip", request.Skip);
+ parameters.Add("Take", request.Take);
+
+ if (request.StatusFilter.HasValue)
+ {
+ whereClause += " AND o.Status = @Status";
+ parameters.Add("Status", request.StatusFilter.Value.ToString());
+ }
+
+ // EN: Count total
+ // VI: Đếm tổng
+ var countSql = $"SELECT COUNT(*) FROM Orders o {whereClause}";
+ var total = await _connection.ExecuteScalarAsync(countSql, parameters);
+
+ // EN: Get data with pagination
+ // VI: Lấy dữ liệu với phân trang
+ var dataSql = $@"
+ SELECT
+ o.Id,
+ o.Status,
+ o.TotalAmount,
+ o.CreatedAt,
+ (SELECT COUNT(*) FROM OrderItems WHERE OrderId = o.Id) as ItemCount
+ FROM Orders o
+ {whereClause}
+ ORDER BY o.CreatedAt DESC
+ OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY";
+
+ var orders = await _connection.QueryAsync(dataSql, parameters);
+
+ return new PagedResult(
+ orders.ToList(),
+ total,
+ request.Skip / request.Take + 1,
+ request.Take);
+ }
+}
+```
+
+### Complex Query with Multi-Mapping
+
+```csharp
+///
+/// EN: Query for order detail with items.
+/// VI: Query cho chi tiết order với items.
+///
+public record GetOrderDetailQuery(
+ Guid OrderId,
+ string UserId) : IRequest;
+
+public class GetOrderDetailQueryHandler
+ : IRequestHandler
+{
+ private readonly IDbConnection _connection;
+
+ public async Task Handle(
+ GetOrderDetailQuery request,
+ CancellationToken ct)
+ {
+ const string sql = @"
+ SELECT
+ o.Id, o.UserId, o.Status, o.TotalAmount, o.CreatedAt,
+ o.ShippingAddress_Street as Street,
+ o.ShippingAddress_City as City,
+ o.ShippingAddress_PostalCode as PostalCode,
+ o.ShippingAddress_Country as Country,
+ oi.Id as ItemId, oi.ProductId, oi.Quantity, oi.UnitPrice,
+ p.Name as ProductName
+ FROM Orders o
+ LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
+ LEFT JOIN Products p ON oi.ProductId = p.Id
+ WHERE o.Id = @OrderId AND o.UserId = @UserId";
+
+ var orderDict = new Dictionary();
+
+ await _connection.QueryAsync(
+ sql,
+ (order, item) =>
+ {
+ if (!orderDict.TryGetValue(order.Id, out var existingOrder))
+ {
+ existingOrder = order;
+ existingOrder.Items = new List();
+ orderDict.Add(order.Id, existingOrder);
+ }
+
+ if (item != null)
+ existingOrder.Items.Add(item);
+
+ return existingOrder;
+ },
+ new { request.OrderId, request.UserId },
+ splitOn: "ItemId");
+
+ return orderDict.Values.FirstOrDefault();
+ }
+}
+```
+
+---
+
+## Pipeline Behaviors
+
+### Complete Validation Behavior
+
+```csharp
+///
+/// EN: Validation behavior with detailed error mapping.
+/// VI: Validation behavior với ánh xạ lỗi chi tiết.
+///
+public class ValidationBehavior
+ : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly IEnumerable> _validators;
+ private readonly ILogger> _logger;
+
+ public ValidationBehavior(
+ IEnumerable> validators,
+ ILogger> logger)
+ {
+ _validators = validators;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken ct)
+ {
+ var typeName = typeof(TRequest).Name;
+
+ if (!_validators.Any())
+ {
+ _logger.LogDebug("No validators for {RequestType}", typeName);
+ return await next();
+ }
+
+ var context = new ValidationContext(request);
+
+ var validationResults = await Task.WhenAll(
+ _validators.Select(v => v.ValidateAsync(context, ct)));
+
+ var failures = validationResults
+ .SelectMany(r => r.Errors)
+ .Where(f => f != null)
+ .ToList();
+
+ if (failures.Count != 0)
+ {
+ _logger.LogWarning(
+ "Validation failed for {RequestType}: {Errors}",
+ typeName,
+ string.Join(", ", failures.Select(f => f.ErrorMessage)));
+
+ throw new ValidationException(failures);
+ }
+
+ return await next();
+ }
+}
+```
+
+### Logging Behavior with Performance Tracking
+
+```csharp
+///
+/// EN: Logging behavior with performance alerts.
+/// VI: Logging behavior với cảnh báo hiệu năng.
+///
+public class LoggingBehavior
+ : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly ILogger> _logger;
+ private readonly TimeSpan _slowThreshold = TimeSpan.FromSeconds(3);
+
+ public LoggingBehavior(ILogger> logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken ct)
+ {
+ var requestName = typeof(TRequest).Name;
+ var requestId = Guid.NewGuid().ToString("N")[..8];
+
+ _logger.LogInformation(
+ "[{RequestId}] Handling {RequestName}",
+ requestId, requestName);
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ var response = await next();
+ stopwatch.Stop();
+
+ if (stopwatch.Elapsed > _slowThreshold)
+ {
+ _logger.LogWarning(
+ "[{RequestId}] SLOW: {RequestName} took {ElapsedMs}ms",
+ requestId, requestName, stopwatch.ElapsedMilliseconds);
+ }
+ else
+ {
+ _logger.LogInformation(
+ "[{RequestId}] Completed {RequestName} in {ElapsedMs}ms",
+ requestId, requestName, stopwatch.ElapsedMilliseconds);
+ }
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ _logger.LogError(ex,
+ "[{RequestId}] FAILED: {RequestName} after {ElapsedMs}ms",
+ requestId, requestName, stopwatch.ElapsedMilliseconds);
+ throw;
+ }
+ }
+}
+```
+
+### Transaction Behavior
+
+```csharp
+///
+/// EN: Transaction behavior for commands.
+/// VI: Transaction behavior cho commands.
+///
+public class TransactionBehavior
+ : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly ApplicationDbContext _dbContext;
+ private readonly ILogger> _logger;
+
+ public TransactionBehavior(
+ ApplicationDbContext dbContext,
+ ILogger> logger)
+ {
+ _dbContext = dbContext;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken ct)
+ {
+ // EN: Only wrap commands in transactions
+ // VI: Chỉ wrap commands trong transactions
+ if (!IsCommand())
+ return await next();
+
+ var typeName = typeof(TRequest).Name;
+
+ await using var transaction = await _dbContext.Database
+ .BeginTransactionAsync(ct);
+
+ try
+ {
+ _logger.LogDebug("Begin transaction for {RequestType}", typeName);
+
+ var response = await next();
+
+ await transaction.CommitAsync(ct);
+
+ _logger.LogDebug("Committed transaction for {RequestType}", typeName);
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ await transaction.RollbackAsync(ct);
+ _logger.LogError(ex, "Rolled back transaction for {RequestType}", typeName);
+ throw;
+ }
+ }
+
+ private static bool IsCommand()
+ {
+ return typeof(TRequest).Name.EndsWith("Command");
+ }
+}
+```
+
+---
+
+## Idempotency
+
+### Identified Command Pattern
+
+```csharp
+///
+/// EN: Wrapper for idempotent commands.
+/// VI: Wrapper cho commands idempotent.
+///
+public record IdentifiedCommand(
+ Guid RequestId,
+ TCommand Command) : IRequest
+ where TCommand : IRequest;
+
+///
+/// EN: Handler for identified commands.
+/// VI: Handler cho identified commands.
+///
+public class IdentifiedCommandHandler
+ : IRequestHandler, TResult>
+ where TCommand : IRequest
+{
+ private readonly IMediator _mediator;
+ private readonly IRequestManager _requestManager;
+ private readonly ILogger> _logger;
+
+ public IdentifiedCommandHandler(
+ IMediator mediator,
+ IRequestManager requestManager,
+ ILogger> logger)
+ {
+ _mediator = mediator;
+ _requestManager = requestManager;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ IdentifiedCommand request,
+ CancellationToken ct)
+ {
+ var commandName = typeof(TCommand).Name;
+
+ // EN: Check if already processed
+ // VI: Kiểm tra đã xử lý chưa
+ if (await _requestManager.ExistsAsync(request.RequestId, ct))
+ {
+ _logger.LogWarning(
+ "Duplicate request {RequestId} for {CommandName}",
+ request.RequestId, commandName);
+
+ return default!; // EN: Return cached result if needed
+ }
+
+ // EN: Mark as processing
+ // VI: Đánh dấu đang xử lý
+ await _requestManager.CreateAsync(request.RequestId, commandName, ct);
+
+ try
+ {
+ var result = await _mediator.Send(request.Command, ct);
+
+ _logger.LogInformation(
+ "Processed {CommandName} with RequestId {RequestId}",
+ commandName, request.RequestId);
+
+ return result;
+ }
+ catch
+ {
+ await _requestManager.DeleteAsync(request.RequestId, ct);
+ throw;
+ }
+ }
+}
+```
+
+### Request Manager
+
+```csharp
+///
+/// EN: Interface for request deduplication.
+/// VI: Interface cho request deduplication.
+///
+public interface IRequestManager
+{
+ Task ExistsAsync(Guid requestId, CancellationToken ct);
+ Task CreateAsync(Guid requestId, string commandName, CancellationToken ct);
+ Task DeleteAsync(Guid requestId, CancellationToken ct);
+}
+
+///
+/// EN: Database-backed request manager.
+/// VI: Request manager với database.
+///
+public class RequestManager : IRequestManager
+{
+ private readonly ApplicationDbContext _context;
+
+ public RequestManager(ApplicationDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task ExistsAsync(Guid requestId, CancellationToken ct)
+ {
+ return await _context.ClientRequests
+ .AnyAsync(r => r.Id == requestId, ct);
+ }
+
+ public async Task CreateAsync(Guid requestId, string commandName, CancellationToken ct)
+ {
+ var request = new ClientRequest
+ {
+ Id = requestId,
+ Name = commandName,
+ Time = DateTime.UtcNow
+ };
+
+ _context.ClientRequests.Add(request);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task DeleteAsync(Guid requestId, CancellationToken ct)
+ {
+ var request = await _context.ClientRequests.FindAsync(new object[] { requestId }, ct);
+ if (request != null)
+ {
+ _context.ClientRequests.Remove(request);
+ await _context.SaveChangesAsync(ct);
+ }
+ }
+}
+
+///
+/// EN: Entity for tracking processed requests.
+/// VI: Entity theo dõi requests đã xử lý.
+///
+public class ClientRequest
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public DateTime Time { get; set; }
+}
+```
+
+---
+
+## Domain Events
+
+### Publishing Domain Events
+
+```csharp
+///
+/// EN: Domain event interface.
+/// VI: Interface domain event.
+///
+public interface IDomainEvent : INotification
+{
+ DateTime OccurredOn { get; }
+}
+
+///
+/// EN: Order created domain event.
+/// VI: Domain event order được tạo.
+///
+public record OrderCreatedEvent(
+ Guid OrderId,
+ string UserId,
+ decimal TotalAmount) : IDomainEvent
+{
+ public DateTime OccurredOn { get; } = DateTime.UtcNow;
+}
+
+///
+/// EN: Handler for OrderCreatedEvent.
+/// VI: Handler cho OrderCreatedEvent.
+///
+public class OrderCreatedEventHandler : INotificationHandler
+{
+ private readonly IEmailService _emailService;
+ private readonly ILogger _logger;
+
+ public OrderCreatedEventHandler(
+ IEmailService emailService,
+ ILogger logger)
+ {
+ _emailService = emailService;
+ _logger = logger;
+ }
+
+ public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
+ {
+ _logger.LogInformation(
+ "Sending order confirmation email for Order {OrderId}",
+ notification.OrderId);
+
+ await _emailService.SendOrderConfirmationAsync(
+ notification.UserId,
+ notification.OrderId,
+ notification.TotalAmount,
+ ct);
+ }
+}
+```
+
+---
+
+## Resources / Tài Nguyên
+
+- [MediatR Documentation](https://github.com/jbogard/MediatR)
+- [FluentValidation](https://docs.fluentvalidation.net/)
+- [CQRS Pattern - Microsoft](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+- [eShopOnContainers - CQRS](https://github.com/dotnet-architecture/eShopOnContainers)
diff --git a/.agent/skills/docker-traefik/SKILL.md b/.agent/skills/docker-traefik/SKILL.md
new file mode 100644
index 00000000..cf20ad04
--- /dev/null
+++ b/.agent/skills/docker-traefik/SKILL.md
@@ -0,0 +1,401 @@
+---
+name: docker-traefik
+description: Docker containerization và Traefik reverse proxy. Use for Dockerfile, docker-compose, routing rules, SSL termination, và load balancing.
+compatibility: "Docker 24+, Docker Compose v2+, Traefik v3+"
+metadata:
+ author: Velik Ho
+ version: "1.0"
+---
+
+# Docker & Traefik Patterns / Mẫu Docker & Traefik
+
+Docker containerization và Traefik reverse proxy cho GoodGo microservices.
+
+## When to Use This Skill / Khi Nào Sử Dụng
+
+Use this skill when:
+- Creating Dockerfiles for .NET services / Tạo Dockerfiles cho services .NET
+- Configuring docker-compose for local dev / Cấu hình docker-compose cho dev local
+- Setting up Traefik routing / Cài đặt Traefik routing
+- Configuring SSL/TLS termination / Cấu hình SSL/TLS termination
+- Implementing load balancing / Triển khai load balancing
+- Managing service discovery / Quản lý service discovery
+
+## Core Concepts / Khái Niệm Cốt Lõi
+
+### Architecture Overview / Tổng Quan Kiến Trúc
+
+```
+Internet
+ │
+ ▼
+┌─────────────────────────────────────────────────────┐
+│ Traefik (Gateway) │
+│ - SSL Termination - Rate Limiting │
+│ - Load Balancing - Path Routing │
+└───────┬────────────────────────────────┬────────────┘
+ │ │
+ ▼ ▼
+┌───────────────┐ ┌───────────────┐
+│ iam-service │ │ storage-svc │
+│ :8080 │ │ :8080 │
+└───────────────┘ └───────────────┘
+```
+
+### Traefik Concepts / Các Khái Niệm Traefik
+
+| Concept | Description | Example |
+|---------|-------------|---------|
+| **EntryPoints** | Network ports | `web:80`, `websecure:443` |
+| **Routers** | Match requests to services | `PathPrefix(\`/api/v1/iam\`)` |
+| **Services** | Backend targets | `loadbalancer.server.port=8080` |
+| **Middlewares** | Request/Response modifiers | `stripprefix`, `ratelimit` |
+
+### Docker Best Practices / Best Practices Docker
+
+1. **Multi-stage builds** - Separate build and runtime
+2. **Non-root user** - Security best practice
+3. **Alpine images** - Smaller image size
+4. **.dockerignore** - Exclude unnecessary files
+5. **Layer caching** - Optimize build time
+
+## Key Patterns / Mẫu Chính
+
+### Dockerfile for .NET Service
+
+```dockerfile
+# ===================================
+# EN: Build stage / VI: Stage build
+# ===================================
+FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
+WORKDIR /src
+
+# EN: Copy solution and restore dependencies
+# VI: Copy solution và restore dependencies
+COPY *.slnx ./
+COPY src/MyService.API/*.csproj src/MyService.API/
+COPY src/MyService.Domain/*.csproj src/MyService.Domain/
+COPY src/MyService.Infrastructure/*.csproj src/MyService.Infrastructure/
+RUN dotnet restore
+
+# EN: Copy source code and build
+# VI: Copy source code và build
+COPY src/ src/
+RUN dotnet publish src/MyService.API -c Release -o /app --no-restore
+
+# ===================================
+# EN: Runtime stage / VI: Stage runtime
+# ===================================
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
+WORKDIR /app
+
+# EN: Install additional packages if needed
+# VI: Cài đặt packages bổ sung nếu cần
+RUN apk add --no-cache icu-libs
+
+# EN: Create non-root user for security
+# VI: Tạo user không phải root để bảo mật
+RUN addgroup -S appgroup && adduser -S appuser -G appgroup
+USER appuser
+
+# EN: Copy published files
+# VI: Copy files đã publish
+COPY --from=build --chown=appuser:appgroup /app .
+
+# EN: Configure environment
+# VI: Cấu hình môi trường
+ENV ASPNETCORE_URLS=http://+:8080
+ENV ASPNETCORE_ENVIRONMENT=Production
+ENV DOTNET_RUNNING_IN_CONTAINER=true
+
+EXPOSE 8080
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1
+
+ENTRYPOINT ["dotnet", "MyService.API.dll"]
+```
+
+### Docker Compose với Traefik
+
+```yaml
+# docker-compose.yml
+version: "3.8"
+
+services:
+ # ===================================
+ # Traefik - API Gateway
+ # ===================================
+ traefik:
+ image: traefik:v3.0
+ container_name: traefik
+ command:
+ # EN: Enable API and Dashboard
+ # VI: Bật API và Dashboard
+ - "--api.dashboard=true"
+ - "--api.insecure=true"
+
+ # EN: Docker provider configuration
+ # VI: Cấu hình Docker provider
+ - "--providers.docker=true"
+ - "--providers.docker.exposedbydefault=false"
+ - "--providers.docker.network=goodgo-network"
+
+ # EN: Entrypoints
+ # VI: Các điểm vào
+ - "--entrypoints.web.address=:80"
+ - "--entrypoints.websecure.address=:443"
+
+ # EN: Access logs
+ # VI: Logs truy cập
+ - "--accesslog=true"
+ - "--accesslog.format=json"
+ ports:
+ - "80:80"
+ - "443:443"
+ - "8080:8080" # Dashboard
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ networks:
+ - goodgo-network
+ labels:
+ # EN: Enable Traefik dashboard
+ # VI: Bật Traefik dashboard
+ - "traefik.enable=true"
+ - "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)"
+ - "traefik.http.routers.dashboard.service=api@internal"
+
+ # ===================================
+ # IAM Service
+ # ===================================
+ iam-service-net:
+ build:
+ context: ../..
+ dockerfile: services/iam-service-net/Dockerfile
+ container_name: iam-service-net
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ConnectionStrings__DefaultConnection=${IAM_DATABASE_URL}
+ labels:
+ - "traefik.enable=true"
+ # EN: Router configuration / VI: Cấu hình router
+ - "traefik.http.routers.iam-service-net.rule=PathPrefix(`/api/v1/iam`)"
+ - "traefik.http.routers.iam-service-net.entrypoints=web"
+ # EN: Service configuration / VI: Cấu hình service
+ - "traefik.http.services.iam-service-net.loadbalancer.server.port=8080"
+ # EN: Health check / VI: Kiểm tra sức khỏe
+ - "traefik.http.services.iam-service-net.loadbalancer.healthcheck.path=/health/live"
+ - "traefik.http.services.iam-service-net.loadbalancer.healthcheck.interval=10s"
+ networks:
+ - goodgo-network
+ depends_on:
+ - postgres
+
+ # ===================================
+ # Storage Service
+ # ===================================
+ storage-service-net:
+ build:
+ context: ../..
+ dockerfile: services/storage-service-net/Dockerfile
+ container_name: storage-service-net
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ConnectionStrings__DefaultConnection=${STORAGE_DATABASE_URL}
+ - MinIO__Endpoint=${MINIO_ENDPOINT}
+ - MinIO__AccessKey=${MINIO_ACCESS_KEY}
+ - MinIO__SecretKey=${MINIO_SECRET_KEY}
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.storage-service-net.rule=PathPrefix(`/api/v1/storage`)"
+ - "traefik.http.routers.storage-service-net.entrypoints=web"
+ - "traefik.http.services.storage-service-net.loadbalancer.server.port=8080"
+ networks:
+ - goodgo-network
+ depends_on:
+ - postgres
+ - minio
+
+ # ===================================
+ # PostgreSQL
+ # ===================================
+ postgres:
+ image: postgres:15-alpine
+ container_name: postgres
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=postgres
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ networks:
+ - goodgo-network
+
+ # ===================================
+ # MinIO (S3-compatible storage)
+ # ===================================
+ minio:
+ image: minio/minio:latest
+ container_name: minio
+ command: server /data --console-address ":9001"
+ environment:
+ - MINIO_ROOT_USER=minioadmin
+ - MINIO_ROOT_PASSWORD=minioadmin
+ volumes:
+ - minio_data:/data
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ networks:
+ - goodgo-network
+
+networks:
+ goodgo-network:
+ driver: bridge
+
+volumes:
+ postgres_data:
+ minio_data:
+```
+
+### Traefik Middlewares
+
+```yaml
+# EN: Strip path prefix middleware
+# VI: Middleware bỏ prefix path
+services:
+ iam-service-net:
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.iam-service-net.rule=PathPrefix(`/api/v1/iam`)"
+ - "traefik.http.routers.iam-service-net.entrypoints=web"
+ # EN: Apply middleware to strip prefix
+ # VI: Áp dụng middleware để bỏ prefix
+ - "traefik.http.routers.iam-service-net.middlewares=iam-stripprefix"
+ - "traefik.http.middlewares.iam-stripprefix.stripprefix.prefixes=/api/v1/iam"
+ - "traefik.http.services.iam-service-net.loadbalancer.server.port=8080"
+```
+
+### Rate Limiting Middleware
+
+```yaml
+services:
+ api-service:
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.api.rule=PathPrefix(`/api`)"
+ # EN: Rate limiting middleware
+ # VI: Middleware giới hạn rate
+ - "traefik.http.routers.api.middlewares=api-ratelimit"
+ - "traefik.http.middlewares.api-ratelimit.ratelimit.average=100"
+ - "traefik.http.middlewares.api-ratelimit.ratelimit.burst=50"
+ - "traefik.http.middlewares.api-ratelimit.ratelimit.period=1m"
+```
+
+## Common Mistakes / Lỗi Thường Gặp
+
+### 1. Running as Root
+
+```dockerfile
+# ❌ BAD: Running as root
+FROM mcr.microsoft.com/dotnet/aspnet:8.0
+COPY --from=build /app .
+ENTRYPOINT ["dotnet", "MyService.dll"]
+
+# ✅ GOOD: Non-root user
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
+RUN adduser -D appuser
+USER appuser
+COPY --from=build --chown=appuser /app .
+ENTRYPOINT ["dotnet", "MyService.dll"]
+```
+
+### 2. No Health Checks
+
+```yaml
+# ❌ BAD: No health check
+services:
+ my-service:
+ build: .
+ labels:
+ - "traefik.enable=true"
+
+# ✅ GOOD: With health check
+services:
+ my-service:
+ build: .
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.services.my-service.loadbalancer.healthcheck.path=/health/live"
+ - "traefik.http.services.my-service.loadbalancer.healthcheck.interval=10s"
+```
+
+### 3. Exposing Internal Ports
+
+```yaml
+# ❌ BAD: Exposing all ports
+services:
+ my-service:
+ ports:
+ - "8080:8080" # Direct access bypasses Traefik
+
+# ✅ GOOD: Only expose through Traefik
+services:
+ my-service:
+ # No ports exposed directly
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.services.my-service.loadbalancer.server.port=8080"
+```
+
+## Quick Reference / Tham Chiếu Nhanh
+
+### Docker Commands
+
+```bash
+# EN: Build and start / VI: Build và start
+docker-compose -f deployments/local/docker-compose.yml up -d --build
+
+# EN: View logs / VI: Xem logs
+docker-compose logs -f iam-service-net
+
+# EN: Restart service / VI: Restart service
+docker-compose restart iam-service-net
+
+# EN: Stop all / VI: Dừng tất cả
+docker-compose down
+
+# EN: Clean rebuild / VI: Rebuild sạch
+docker-compose down -v && docker-compose up -d --build
+```
+
+### Traefik Router Rules
+
+| Rule | Example | Description |
+|------|---------|-------------|
+| `Host` | ``Host(`api.example.com`)`` | Match by hostname |
+| `PathPrefix` | ``PathPrefix(`/api`)`` | Match by path prefix |
+| `Path` | ``Path(`/api/health`)`` | Match exact path |
+| `Method` | ``Method(`GET`, `POST`)`` | Match by HTTP method |
+| `Headers` | ``Headers(`X-Custom`, `value`)`` | Match by header |
+
+### Common Labels
+
+```yaml
+# EN: Basic routing / VI: Routing cơ bản
+- "traefik.enable=true"
+- "traefik.http.routers.{name}.rule=PathPrefix(`/path`)"
+- "traefik.http.routers.{name}.entrypoints=web"
+- "traefik.http.services.{name}.loadbalancer.server.port=8080"
+
+# EN: With HTTPS / VI: Với HTTPS
+- "traefik.http.routers.{name}-secure.rule=PathPrefix(`/path`)"
+- "traefik.http.routers.{name}-secure.entrypoints=websecure"
+- "traefik.http.routers.{name}-secure.tls=true"
+```
+
+## Resources / Tài Nguyên
+
+- [Detailed Examples](./references/REFERENCE.md) - Full configurations
+- [Project Rules](../project-rules/SKILL.md) - Docker naming conventions
+- [Observability](../observability/SKILL.md) - Container monitoring
+- [Error Handling](../error-handling-patterns/SKILL.md) - Health checks
diff --git a/.agent/skills/docker-traefik/references/REFERENCE.md b/.agent/skills/docker-traefik/references/REFERENCE.md
new file mode 100644
index 00000000..32f72c1d
--- /dev/null
+++ b/.agent/skills/docker-traefik/references/REFERENCE.md
@@ -0,0 +1,560 @@
+# Docker & Traefik - Detailed Reference
+
+Detailed configurations và examples cho Docker và Traefik trong GoodGo.
+
+## Table of Contents
+
+1. [Dockerfile Patterns](#dockerfile-patterns)
+2. [Docker Compose Configurations](#docker-compose-configurations)
+3. [Traefik Configuration](#traefik-configuration)
+4. [SSL/TLS Setup](#ssltls-setup)
+5. [Load Balancing](#load-balancing)
+6. [Middleware Examples](#middleware-examples)
+7. [Development Environment](#development-environment)
+
+---
+
+## Dockerfile Patterns
+
+### Optimized .NET Dockerfile
+
+```dockerfile
+# ===================================
+# Stage 1: Build
+# ===================================
+FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
+WORKDIR /src
+
+# EN: Copy only project files first for better caching
+# VI: Copy chỉ project files trước để cache tốt hơn
+COPY services/iam-service-net/*.slnx ./
+COPY services/iam-service-net/src/IamService.API/*.csproj src/IamService.API/
+COPY services/iam-service-net/src/IamService.Domain/*.csproj src/IamService.Domain/
+COPY services/iam-service-net/src/IamService.Infrastructure/*.csproj src/IamService.Infrastructure/
+
+# EN: Restore dependencies
+# VI: Restore dependencies
+RUN dotnet restore src/IamService.API
+
+# EN: Copy remaining source files
+# VI: Copy các source files còn lại
+COPY services/iam-service-net/src/ src/
+
+# EN: Build and publish
+# VI: Build và publish
+RUN dotnet publish src/IamService.API \
+ -c Release \
+ -o /app \
+ --no-restore \
+ /p:PublishTrimmed=false \
+ /p:PublishSingleFile=false
+
+# ===================================
+# Stage 2: Runtime
+# ===================================
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
+
+# EN: Install required packages
+# VI: Cài đặt packages cần thiết
+RUN apk add --no-cache \
+ icu-libs \
+ tzdata \
+ && rm -rf /var/cache/apk/*
+
+# EN: Set timezone
+# VI: Đặt timezone
+ENV TZ=Asia/Ho_Chi_Minh
+
+# EN: Create non-root user
+# VI: Tạo user không phải root
+RUN addgroup -g 1000 -S appgroup && \
+ adduser -u 1000 -S appuser -G appgroup
+
+WORKDIR /app
+
+# EN: Copy published files with correct ownership
+# VI: Copy files đã publish với ownership đúng
+COPY --from=build --chown=appuser:appgroup /app .
+
+# EN: Switch to non-root user
+# VI: Chuyển sang user không phải root
+USER appuser
+
+# EN: Environment configuration
+# VI: Cấu hình môi trường
+ENV ASPNETCORE_URLS=http://+:8080 \
+ ASPNETCORE_ENVIRONMENT=Production \
+ DOTNET_RUNNING_IN_CONTAINER=true \
+ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
+
+EXPOSE 8080
+
+# EN: Health check
+# VI: Kiểm tra sức khỏe
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1
+
+ENTRYPOINT ["dotnet", "IamService.API.dll"]
+```
+
+### .dockerignore
+
+```
+# EN: Git / VI: Git
+.git
+.gitignore
+
+# EN: Build outputs / VI: Build outputs
+**/bin/
+**/obj/
+**/out/
+
+# EN: IDE / VI: IDE
+.vs/
+.vscode/
+.idea/
+*.swp
+*.user
+
+# EN: Test results / VI: Kết quả test
+**/TestResults/
+**/coverage/
+
+# EN: Docker / VI: Docker
+**/Dockerfile*
+**/docker-compose*
+**/.dockerignore
+
+# EN: Documentation / VI: Tài liệu
+**/README.md
+**/docs/
+
+# EN: Secrets / VI: Secrets
+**/*.env
+**/appsettings.Development.json
+**/appsettings.Local.json
+```
+
+---
+
+## Docker Compose Configurations
+
+### Complete Development Stack
+
+```yaml
+# deployments/local/docker-compose.yml
+version: "3.8"
+
+services:
+ # ===================================
+ # TRAEFIK - API Gateway
+ # ===================================
+ traefik:
+ image: traefik:v3.0
+ container_name: traefik
+ restart: unless-stopped
+ command:
+ # EN: API and Dashboard
+ - "--api.dashboard=true"
+ - "--api.insecure=true"
+
+ # EN: Docker provider
+ - "--providers.docker=true"
+ - "--providers.docker.exposedbydefault=false"
+ - "--providers.docker.network=goodgo-network"
+
+ # EN: Entrypoints
+ - "--entrypoints.web.address=:80"
+ - "--entrypoints.websecure.address=:443"
+
+ # EN: Logging
+ - "--log.level=INFO"
+ - "--accesslog=true"
+ - "--accesslog.format=json"
+
+ # EN: Metrics
+ - "--metrics.prometheus=true"
+ - "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
+ ports:
+ - "80:80"
+ - "443:443"
+ - "8080:8080"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ./traefik/certs:/certs:ro
+ networks:
+ - goodgo-network
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.traefik.rule=Host(`traefik.localhost`)"
+ - "traefik.http.routers.traefik.service=api@internal"
+
+ # ===================================
+ # IAM SERVICE
+ # ===================================
+ iam-service-net:
+ build:
+ context: ../..
+ dockerfile: services/iam-service-net/Dockerfile
+ container_name: iam-service-net
+ restart: unless-stopped
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ConnectionStrings__DefaultConnection=${IAM_DATABASE_URL}
+ - Jwt__Authority=http://iam-service-net:8080
+ - Jwt__Audience=goodgo-api
+ labels:
+ - "traefik.enable=true"
+ # EN: HTTP Router
+ - "traefik.http.routers.iam-service-net.rule=PathPrefix(`/api/v1/iam`)"
+ - "traefik.http.routers.iam-service-net.entrypoints=web"
+ - "traefik.http.routers.iam-service-net.middlewares=iam-headers"
+ # EN: Service
+ - "traefik.http.services.iam-service-net.loadbalancer.server.port=8080"
+ - "traefik.http.services.iam-service-net.loadbalancer.healthcheck.path=/health/live"
+ - "traefik.http.services.iam-service-net.loadbalancer.healthcheck.interval=10s"
+ # EN: Response headers middleware
+ - "traefik.http.middlewares.iam-headers.headers.customresponseheaders.X-Service-Name=iam-service"
+ networks:
+ - goodgo-network
+ depends_on:
+ postgres:
+ condition: service_healthy
+
+ # ===================================
+ # STORAGE SERVICE
+ # ===================================
+ storage-service-net:
+ build:
+ context: ../..
+ dockerfile: services/storage-service-net/Dockerfile
+ container_name: storage-service-net
+ restart: unless-stopped
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ConnectionStrings__DefaultConnection=${STORAGE_DATABASE_URL}
+ - MinIO__Endpoint=minio:9000
+ - MinIO__AccessKey=${MINIO_ACCESS_KEY}
+ - MinIO__SecretKey=${MINIO_SECRET_KEY}
+ - MinIO__UseSSL=false
+ - IamService__BaseUrl=http://iam-service-net:8080
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.storage-service-net.rule=PathPrefix(`/api/v1/storage`)"
+ - "traefik.http.routers.storage-service-net.entrypoints=web"
+ - "traefik.http.services.storage-service-net.loadbalancer.server.port=8080"
+ - "traefik.http.services.storage-service-net.loadbalancer.healthcheck.path=/health/live"
+ networks:
+ - goodgo-network
+ depends_on:
+ postgres:
+ condition: service_healthy
+ minio:
+ condition: service_started
+
+ # ===================================
+ # POSTGRESQL
+ # ===================================
+ postgres:
+ image: postgres:15-alpine
+ container_name: postgres
+ restart: unless-stopped
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_MULTIPLE_DATABASES=iam_db,storage_db,wallet_db
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./scripts/create-multiple-dbs.sh:/docker-entrypoint-initdb.d/create-multiple-dbs.sh
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ networks:
+ - goodgo-network
+
+ # ===================================
+ # REDIS
+ # ===================================
+ redis:
+ image: redis:7-alpine
+ container_name: redis
+ restart: unless-stopped
+ command: redis-server --appendonly yes
+ volumes:
+ - redis_data:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 3s
+ retries: 5
+ networks:
+ - goodgo-network
+
+ # ===================================
+ # MINIO (S3-compatible)
+ # ===================================
+ minio:
+ image: minio/minio:latest
+ container_name: minio
+ restart: unless-stopped
+ command: server /data --console-address ":9001"
+ environment:
+ - MINIO_ROOT_USER=minioadmin
+ - MINIO_ROOT_PASSWORD=minioadmin
+ volumes:
+ - minio_data:/data
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ networks:
+ - goodgo-network
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.minio-console.rule=Host(`minio.localhost`)"
+ - "traefik.http.routers.minio-console.service=minio-console"
+ - "traefik.http.services.minio-console.loadbalancer.server.port=9001"
+
+networks:
+ goodgo-network:
+ driver: bridge
+ name: goodgo-network
+
+volumes:
+ postgres_data:
+ redis_data:
+ minio_data:
+```
+
+### Multi-Database Init Script
+
+```bash
+#!/bin/bash
+# scripts/create-multiple-dbs.sh
+
+set -e
+set -u
+
+function create_user_and_database() {
+ local database=$1
+ echo " Creating database '$database'"
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
+ CREATE DATABASE $database;
+ GRANT ALL PRIVILEGES ON DATABASE $database TO $POSTGRES_USER;
+EOSQL
+}
+
+if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
+ echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES"
+ for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
+ create_user_and_database $db
+ done
+ echo "Multiple databases created"
+fi
+```
+
+---
+
+## Traefik Configuration
+
+### Static Configuration (traefik.yml)
+
+```yaml
+# infra/traefik/traefik.yml
+
+# EN: API and Dashboard
+# VI: API và Dashboard
+api:
+ dashboard: true
+ insecure: false
+
+# EN: Entrypoints
+# VI: Các điểm vào
+entryPoints:
+ web:
+ address: ":80"
+ http:
+ redirections:
+ entryPoint:
+ to: websecure
+ scheme: https
+ websecure:
+ address: ":443"
+ http:
+ tls:
+ certResolver: letsencrypt
+
+# EN: Certificate resolvers
+# VI: Bộ giải quyết chứng chỉ
+certificatesResolvers:
+ letsencrypt:
+ acme:
+ email: admin@goodgo.vn
+ storage: /certs/acme.json
+ httpChallenge:
+ entryPoint: web
+
+# EN: Providers
+# VI: Providers
+providers:
+ docker:
+ exposedByDefault: false
+ network: goodgo-network
+ file:
+ directory: /etc/traefik/dynamic
+ watch: true
+
+# EN: Logging
+# VI: Logging
+log:
+ level: INFO
+ format: json
+
+accessLog:
+ format: json
+ filters:
+ statusCodes:
+ - "400-599"
+
+# EN: Metrics
+# VI: Metrics
+metrics:
+ prometheus:
+ buckets:
+ - 0.1
+ - 0.3
+ - 1.2
+ - 5.0
+```
+
+### Dynamic Configuration
+
+```yaml
+# infra/traefik/dynamic/middlewares.yml
+
+http:
+ middlewares:
+ # EN: Security headers
+ # VI: Headers bảo mật
+ secure-headers:
+ headers:
+ frameDeny: true
+ sslRedirect: true
+ browserXssFilter: true
+ contentTypeNosniff: true
+ forceSTSHeader: true
+ stsIncludeSubdomains: true
+ stsPreload: true
+ stsSeconds: 31536000
+ customResponseHeaders:
+ X-Robots-Tag: "noindex,nofollow"
+
+ # EN: Rate limiting
+ # VI: Giới hạn rate
+ rate-limit:
+ rateLimit:
+ average: 100
+ burst: 50
+ period: 1m
+
+ # EN: Compression
+ # VI: Nén
+ compress:
+ compress: {}
+
+ # EN: CORS
+ # VI: CORS
+ cors:
+ headers:
+ accessControlAllowMethods:
+ - GET
+ - POST
+ - PUT
+ - DELETE
+ - OPTIONS
+ accessControlAllowHeaders:
+ - "*"
+ accessControlAllowOriginList:
+ - "https://app.goodgo.vn"
+ - "http://localhost:3000"
+ accessControlMaxAge: 100
+ addVaryHeader: true
+```
+
+---
+
+## SSL/TLS Setup
+
+### Local Development with mkcert
+
+```bash
+# EN: Install mkcert / VI: Cài đặt mkcert
+brew install mkcert
+mkcert -install
+
+# EN: Generate certificates / VI: Tạo certificates
+cd infra/traefik/certs
+mkcert "*.localhost" localhost 127.0.0.1 ::1
+```
+
+### Traefik with Local Certs
+
+```yaml
+# docker-compose.override.yml for local HTTPS
+services:
+ traefik:
+ volumes:
+ - ./infra/traefik/certs:/certs:ro
+ labels:
+ - "traefik.http.routers.traefik.tls=true"
+
+# Dynamic config: infra/traefik/dynamic/tls.yml
+tls:
+ certificates:
+ - certFile: /certs/_wildcard.localhost.pem
+ keyFile: /certs/_wildcard.localhost-key.pem
+ stores:
+ default:
+ defaultCertificate:
+ certFile: /certs/_wildcard.localhost.pem
+ keyFile: /certs/_wildcard.localhost-key.pem
+```
+
+---
+
+## Load Balancing
+
+### Multiple Instances
+
+```yaml
+services:
+ iam-service-net:
+ deploy:
+ replicas: 3
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.iam.rule=PathPrefix(`/api/v1/iam`)"
+ - "traefik.http.services.iam.loadbalancer.server.port=8080"
+ # EN: Sticky sessions (optional)
+ # VI: Sticky sessions (tùy chọn)
+ - "traefik.http.services.iam.loadbalancer.sticky.cookie.name=iam_sticky"
+ - "traefik.http.services.iam.loadbalancer.sticky.cookie.secure=true"
+ # EN: Health check
+ - "traefik.http.services.iam.loadbalancer.healthcheck.path=/health/live"
+ - "traefik.http.services.iam.loadbalancer.healthcheck.interval=5s"
+```
+
+---
+
+## Resources / Tài Nguyên
+
+- [Traefik Documentation](https://doc.traefik.io/traefik/)
+- [Docker Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
+- [.NET Docker Images](https://hub.docker.com/_/microsoft-dotnet)
+- [Docker Compose Specification](https://docs.docker.com/compose/compose-file/)
diff --git a/.agent/skills/error-handling-patterns/SKILL.md b/.agent/skills/error-handling-patterns/SKILL.md
index ef65cf10..83609e0c 100644
--- a/.agent/skills/error-handling-patterns/SKILL.md
+++ b/.agent/skills/error-handling-patterns/SKILL.md
@@ -1,7 +1,7 @@
---
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 8+, Polly, FluentValidation, Microsoft.Extensions.Diagnostics.HealthChecks"
+compatibility: ".NET 10+, Polly, FluentValidation, Microsoft.Extensions.Diagnostics.HealthChecks"
metadata:
author: Velik Ho
version: "1.0"
diff --git a/.agent/skills/observability/SKILL.md b/.agent/skills/observability/SKILL.md
new file mode 100644
index 00000000..108d6dfa
--- /dev/null
+++ b/.agent/skills/observability/SKILL.md
@@ -0,0 +1,451 @@
+---
+name: observability
+description: Monitoring, logging, và tracing patterns. Use for Prometheus metrics, Grafana dashboards, Loki logging, và distributed tracing.
+compatibility: ".NET 10+, Serilog, OpenTelemetry, Prometheus, Grafana"
+metadata:
+ author: Velik Ho
+ version: "1.0"
+---
+
+# Observability Patterns / Mẫu Quan Sát
+
+Monitoring, logging, và tracing cho GoodGo microservices.
+
+## When to Use This Skill / Khi Nào Sử Dụng
+
+Use this skill when:
+- Implementing health checks / Triển khai health checks
+- Configuring structured logging / Cấu hình structured logging
+- Setting up Prometheus metrics / Cài đặt Prometheus metrics
+- Implementing distributed tracing / Triển khai distributed tracing
+- Creating Grafana dashboards / Tạo Grafana dashboards
+
+## Core Concepts / Khái Niệm Cốt Lõi
+
+### Three Pillars of Observability / Ba Trụ Cột của Observability
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ OBSERVABILITY │
+├───────────────────┬───────────────────┬───────────────────┤
+│ METRICS │ LOGS │ TRACES │
+│ (Prometheus) │ (Loki/ELK) │ (OpenTelemetry) │
+├───────────────────┼───────────────────┼───────────────────┤
+│ What's happening │ What happened │ How it happened │
+│ (Số liệu) │ (Nhật ký) │ (Theo dõi) │
+└───────────────────┴───────────────────┴───────────────────┘
+```
+
+### Health Check Types / Các Loại Health Check
+
+| Type | Purpose | Endpoint |
+|------|---------|----------|
+| **Liveness** | Is the app running? | `/health/live` |
+| **Readiness** | Can the app accept traffic? | `/health/ready` |
+| **Startup** | Has the app initialized? | `/health/startup` |
+
+### Logging Levels / Các Mức Log
+
+| Level | Purpose | Example |
+|-------|---------|---------|
+| **Trace** | Detailed debugging | Variable values |
+| **Debug** | Development info | Method entry/exit |
+| **Information** | Normal operations | Request processed |
+| **Warning** | Potential problems | Slow query |
+| **Error** | Errors handled | Exception caught |
+| **Critical** | System failures | Database down |
+
+## Key Patterns / Mẫu Chính
+
+### Health Checks Configuration
+
+```csharp
+///
+/// EN: Configure comprehensive health checks.
+/// VI: Cấu hình health checks toàn diện.
+///
+
+// Program.cs
+builder.Services.AddHealthChecks()
+ // EN: Database health / VI: Health database
+ .AddNpgSql(
+ connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
+ name: "postgresql",
+ failureStatus: HealthStatus.Unhealthy,
+ tags: new[] { "db", "ready" })
+
+ // EN: Redis cache / VI: Redis cache
+ .AddRedis(
+ redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
+ name: "redis",
+ failureStatus: HealthStatus.Degraded,
+ tags: new[] { "cache", "ready" })
+
+ // EN: Custom health check / VI: Health check tùy chỉnh
+ .AddCheck(
+ "external-api",
+ failureStatus: HealthStatus.Degraded,
+ tags: new[] { "external", "ready" });
+
+// EN: Map health endpoints / VI: Map health endpoints
+app.MapHealthChecks("/health/live", new HealthCheckOptions
+{
+ Predicate = _ => false, // EN: Just check app is running
+ ResponseWriter = WriteMinimalResponse
+});
+
+app.MapHealthChecks("/health/ready", new HealthCheckOptions
+{
+ Predicate = check => check.Tags.Contains("ready"),
+ ResponseWriter = WriteDetailedResponse
+});
+
+app.MapHealthChecks("/health", new HealthCheckOptions
+{
+ ResponseWriter = WriteDetailedResponse
+});
+```
+
+### Structured Logging with Serilog
+
+```csharp
+///
+/// EN: Configure Serilog with structured logging.
+/// VI: Cấu hình Serilog với structured logging.
+///
+
+// Program.cs
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Information()
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
+ .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
+ .Enrich.FromLogContext()
+ .Enrich.WithMachineName()
+ .Enrich.WithEnvironmentName()
+ .Enrich.WithProperty("Application", "IamService")
+ .WriteTo.Console(new JsonFormatter())
+ .WriteTo.Seq(builder.Configuration["Seq:ServerUrl"]!)
+ .CreateLogger();
+
+builder.Host.UseSerilog();
+
+// EN: Usage in code / VI: Sử dụng trong code
+public class OrderService
+{
+ private readonly ILogger _logger;
+
+ public async Task CreateOrderAsync(CreateOrderCommand cmd)
+ {
+ // EN: Structured logging with properties
+ // VI: Structured logging với properties
+ _logger.LogInformation(
+ "Creating order for user {UserId} with {ItemCount} items",
+ cmd.UserId,
+ cmd.Items.Count);
+
+ try
+ {
+ var order = await ProcessOrderAsync(cmd);
+
+ _logger.LogInformation(
+ "Order {OrderId} created successfully. Total: {TotalAmount}",
+ order.Id,
+ order.TotalAmount);
+
+ return order;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex,
+ "Failed to create order for user {UserId}",
+ cmd.UserId);
+ throw;
+ }
+ }
+}
+```
+
+### Prometheus Metrics
+
+```csharp
+///
+/// EN: Configure Prometheus metrics.
+/// VI: Cấu hình Prometheus metrics.
+///
+
+// Program.cs
+builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation()
+ .AddPrometheusExporter();
+ });
+
+// EN: Map Prometheus endpoint / VI: Map Prometheus endpoint
+app.MapPrometheusScrapingEndpoint();
+
+///
+/// EN: Custom metrics for business logic.
+/// VI: Metrics tùy chỉnh cho business logic.
+///
+public class OrderMetrics
+{
+ private readonly Counter _ordersCreated;
+ private readonly Histogram _orderProcessingDuration;
+ private readonly UpDownCounter _activeOrders;
+
+ public OrderMetrics(IMeterFactory meterFactory)
+ {
+ var meter = meterFactory.Create("GoodGo.Orders");
+
+ _ordersCreated = meter.CreateCounter(
+ "orders_created_total",
+ description: "Total number of orders created");
+
+ _orderProcessingDuration = meter.CreateHistogram(
+ "order_processing_duration_seconds",
+ description: "Duration of order processing in seconds");
+
+ _activeOrders = meter.CreateUpDownCounter(
+ "active_orders",
+ description: "Number of orders currently being processed");
+ }
+
+ public void OrderCreated(string status)
+ {
+ _ordersCreated.Add(1, new KeyValuePair("status", status));
+ }
+
+ public void RecordProcessingDuration(double seconds)
+ {
+ _orderProcessingDuration.Record(seconds);
+ }
+
+ public void IncrementActiveOrders() => _activeOrders.Add(1);
+ public void DecrementActiveOrders() => _activeOrders.Add(-1);
+}
+```
+
+### Distributed Tracing with OpenTelemetry
+
+```csharp
+///
+/// EN: Configure OpenTelemetry distributed tracing.
+/// VI: Cấu hình OpenTelemetry distributed tracing.
+///
+
+// Program.cs
+builder.Services.AddOpenTelemetry()
+ .WithTracing(tracing =>
+ {
+ tracing
+ .SetResourceBuilder(ResourceBuilder.CreateDefault()
+ .AddService("iam-service")
+ .AddAttributes(new[]
+ {
+ new KeyValuePair("deployment.environment",
+ builder.Environment.EnvironmentName)
+ }))
+ .AddAspNetCoreInstrumentation(options =>
+ {
+ options.RecordException = true;
+ options.Filter = ctx =>
+ !ctx.Request.Path.StartsWithSegments("/health");
+ })
+ .AddHttpClientInstrumentation()
+ .AddEntityFrameworkCoreInstrumentation()
+ .AddSource("GoodGo.Orders")
+ .AddOtlpExporter(options =>
+ {
+ options.Endpoint = new Uri(builder.Configuration["Otlp:Endpoint"]!);
+ });
+ });
+
+///
+/// EN: Custom activity for business operations.
+/// VI: Activity tùy chỉnh cho business operations.
+///
+public class OrderService
+{
+ private static readonly ActivitySource ActivitySource = new("GoodGo.Orders");
+
+ public async Task ProcessOrderAsync(CreateOrderCommand cmd)
+ {
+ using var activity = ActivitySource.StartActivity("ProcessOrder");
+ activity?.SetTag("user.id", cmd.UserId);
+ activity?.SetTag("order.items_count", cmd.Items.Count);
+
+ try
+ {
+ // EN: Create child span for validation
+ // VI: Tạo child span cho validation
+ using var validationActivity = ActivitySource.StartActivity("ValidateOrder");
+ await ValidateOrderAsync(cmd);
+ validationActivity?.SetStatus(ActivityStatusCode.Ok);
+
+ // EN: Create child span for persistence
+ // VI: Tạo child span cho persistence
+ using var persistActivity = ActivitySource.StartActivity("PersistOrder");
+ var order = await SaveOrderAsync(cmd);
+ persistActivity?.SetTag("order.id", order.Id.ToString());
+
+ activity?.SetTag("order.id", order.Id.ToString());
+ activity?.SetStatus(ActivityStatusCode.Ok);
+
+ return order;
+ }
+ catch (Exception ex)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.RecordException(ex);
+ throw;
+ }
+ }
+}
+```
+
+### Request Logging Middleware
+
+```csharp
+///
+/// EN: Middleware for request/response logging.
+/// VI: Middleware cho logging request/response.
+///
+public class RequestLoggingMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public RequestLoggingMiddleware(
+ RequestDelegate next,
+ ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var requestId = context.TraceIdentifier;
+
+ // EN: Log request / VI: Log request
+ _logger.LogInformation(
+ "Request {RequestId} started: {Method} {Path}",
+ requestId,
+ context.Request.Method,
+ context.Request.Path);
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ stopwatch.Stop();
+
+ // EN: Log response / VI: Log response
+ _logger.LogInformation(
+ "Request {RequestId} completed: {StatusCode} in {ElapsedMs}ms",
+ requestId,
+ context.Response.StatusCode,
+ stopwatch.ElapsedMilliseconds);
+ }
+ }
+}
+```
+
+## Common Mistakes / Lỗi Thường Gặp
+
+### 1. Logging Sensitive Data
+
+```csharp
+// ❌ BAD: Logging password
+_logger.LogInformation("Login: {Email} {Password}", email, password);
+
+// ✅ GOOD: Redact sensitive data
+_logger.LogInformation("Login attempt for {Email}", email);
+```
+
+### 2. Missing Correlation ID
+
+```csharp
+// ❌ BAD: No correlation between logs
+_logger.LogInformation("Processing order");
+_logger.LogInformation("Order completed");
+
+// ✅ GOOD: Include correlation ID
+using (_logger.BeginScope(new Dictionary
+{
+ ["OrderId"] = orderId,
+ ["UserId"] = userId
+}))
+{
+ _logger.LogInformation("Processing order");
+ _logger.LogInformation("Order completed");
+}
+```
+
+### 3. No Health Check for Dependencies
+
+```csharp
+// ❌ BAD: Only check if app runs
+builder.Services.AddHealthChecks();
+
+// ✅ GOOD: Check all dependencies
+builder.Services.AddHealthChecks()
+ .AddNpgSql(connectionString, name: "database")
+ .AddRedis(redisConnection, name: "cache")
+ .AddUrlGroup(new Uri(externalApi), name: "external-api");
+```
+
+## Quick Reference / Tham Chiếu Nhanh
+
+### Serilog Configuration (appsettings.json)
+
+```json
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ { "Name": "Console", "Args": { "formatter": "Serilog.Formatting.Json.JsonFormatter" } },
+ { "Name": "Seq", "Args": { "serverUrl": "http://seq:5341" } }
+ ],
+ "Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"]
+ }
+}
+```
+
+### Health Check Endpoints
+
+| Endpoint | Purpose | K8s Probe |
+|----------|---------|-----------|
+| `/health/live` | App is running | livenessProbe |
+| `/health/ready` | App can accept traffic | readinessProbe |
+| `/health/startup` | App has initialized | startupProbe |
+
+### Common Metrics
+
+| Metric | Type | Description |
+|--------|------|-------------|
+| `http_requests_total` | Counter | Total HTTP requests |
+| `http_request_duration_seconds` | Histogram | Request duration |
+| `dotnet_gc_collections_total` | Counter | GC collections |
+| `process_cpu_seconds_total` | Counter | CPU usage |
+
+## Resources / Tài Nguyên
+
+- [Detailed Examples](./references/REFERENCE.md) - Full configurations
+- [Error Handling](../error-handling-patterns/SKILL.md) - Health checks
+- [Docker Traefik](../docker-traefik/SKILL.md) - Container monitoring
+- [Project Rules](../project-rules/SKILL.md) - Logging standards
diff --git a/.agent/skills/observability/references/REFERENCE.md b/.agent/skills/observability/references/REFERENCE.md
new file mode 100644
index 00000000..f3a8b822
--- /dev/null
+++ b/.agent/skills/observability/references/REFERENCE.md
@@ -0,0 +1,570 @@
+# Observability - Detailed Reference
+
+Detailed configurations và examples cho Observability stack trong GoodGo.
+
+## Table of Contents
+
+1. [Serilog Configuration](#serilog-configuration)
+2. [OpenTelemetry Setup](#opentelemetry-setup)
+3. [Prometheus & Grafana](#prometheus--grafana)
+4. [Health Checks](#health-checks)
+5. [Loki Logging](#loki-logging)
+6. [Alerting](#alerting)
+
+---
+
+## Serilog Configuration
+
+### Complete Program.cs Setup
+
+```csharp
+///
+/// EN: Complete Serilog configuration for microservice.
+/// VI: Serilog configuration đầy đủ cho microservice.
+///
+
+using Serilog;
+using Serilog.Events;
+using Serilog.Formatting.Json;
+using Serilog.Sinks.Grafana.Loki;
+
+// EN: Configure Serilog bootstrap logger for startup errors
+// VI: Cấu hình Serilog bootstrap logger cho lỗi startup
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
+ .Enrich.FromLogContext()
+ .WriteTo.Console()
+ .CreateBootstrapLogger();
+
+try
+{
+ var builder = WebApplication.CreateBuilder(args);
+
+ // EN: Configure Serilog from configuration
+ // VI: Cấu hình Serilog từ configuration
+ builder.Host.UseSerilog((context, services, configuration) => configuration
+ .ReadFrom.Configuration(context.Configuration)
+ .ReadFrom.Services(services)
+ .Enrich.FromLogContext()
+ .Enrich.WithProperty("Application", "IamService")
+ .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
+ .Enrich.WithMachineName()
+ .Enrich.WithThreadId()
+ .WriteTo.Console(new JsonFormatter())
+ .WriteTo.GrafanaLoki(
+ context.Configuration["Loki:Endpoint"]!,
+ labels: new[]
+ {
+ new LokiLabel { Key = "app", Value = "iam-service" },
+ new LokiLabel { Key = "env", Value = context.HostingEnvironment.EnvironmentName }
+ }));
+
+ // ... rest of configuration
+
+ var app = builder.Build();
+
+ // EN: Add Serilog request logging middleware
+ // VI: Thêm Serilog request logging middleware
+ app.UseSerilogRequestLogging(options =>
+ {
+ options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
+ {
+ diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
+ diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
+ diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
+
+ if (httpContext.User.Identity?.IsAuthenticated == true)
+ {
+ diagnosticContext.Set("UserId", httpContext.User.FindFirst("sub")?.Value);
+ }
+ };
+ });
+
+ app.Run();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Application terminated unexpectedly");
+}
+finally
+{
+ Log.CloseAndFlush();
+}
+```
+
+### appsettings.json for Serilog
+
+```json
+{
+ "Serilog": {
+ "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "System": "Warning",
+ "Grpc": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
+ }
+ },
+ {
+ "Name": "Seq",
+ "Args": {
+ "serverUrl": "http://seq:5341",
+ "apiKey": ""
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithMachineName",
+ "WithThreadId",
+ "WithEnvironmentName"
+ ],
+ "Properties": {
+ "Application": "IamService"
+ }
+ }
+}
+```
+
+---
+
+## OpenTelemetry Setup
+
+### Complete OpenTelemetry Configuration
+
+```csharp
+///
+/// EN: Configure OpenTelemetry for tracing and metrics.
+/// VI: Cấu hình OpenTelemetry cho tracing và metrics.
+///
+
+builder.Services.AddOpenTelemetry()
+ .ConfigureResource(resource => resource
+ .AddService(
+ serviceName: "iam-service",
+ serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
+ serviceInstanceId: Environment.MachineName)
+ .AddAttributes(new[]
+ {
+ new KeyValuePair("deployment.environment",
+ builder.Environment.EnvironmentName),
+ new KeyValuePair("host.name", Environment.MachineName)
+ }))
+ .WithTracing(tracing =>
+ {
+ tracing
+ // EN: ASP.NET Core instrumentation
+ .AddAspNetCoreInstrumentation(options =>
+ {
+ options.RecordException = true;
+ options.Filter = ctx =>
+ !ctx.Request.Path.StartsWithSegments("/health") &&
+ !ctx.Request.Path.StartsWithSegments("/metrics");
+ })
+ // EN: HTTP client instrumentation
+ .AddHttpClientInstrumentation(options =>
+ {
+ options.RecordException = true;
+ options.FilterHttpRequestMessage = req =>
+ !req.RequestUri?.Host.Contains("health") ?? true;
+ })
+ // EN: Entity Framework instrumentation
+ .AddEntityFrameworkCoreInstrumentation(options =>
+ {
+ options.SetDbStatementForText = true;
+ options.SetDbStatementForStoredProcedure = true;
+ })
+ // EN: Custom activity sources
+ .AddSource("GoodGo.Iam")
+ .AddSource("GoodGo.Orders")
+ // EN: Export to OTLP (Jaeger/Tempo)
+ .AddOtlpExporter(options =>
+ {
+ options.Endpoint = new Uri(builder.Configuration["Otlp:Endpoint"]!);
+ options.Protocol = OtlpExportProtocol.Grpc;
+ });
+ })
+ .WithMetrics(metrics =>
+ {
+ metrics
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation()
+ // EN: Custom meters
+ .AddMeter("GoodGo.Iam")
+ .AddMeter("GoodGo.Orders")
+ // EN: Prometheus exporter
+ .AddPrometheusExporter();
+ });
+
+// EN: Map Prometheus scraping endpoint
+app.MapPrometheusScrapingEndpoint();
+```
+
+### Custom Activity Source
+
+```csharp
+///
+/// EN: Service with custom tracing.
+/// VI: Service với tracing tùy chỉnh.
+///
+public class OrderService
+{
+ private static readonly ActivitySource ActivitySource = new("GoodGo.Orders");
+ private readonly ILogger _logger;
+
+ public async Task ProcessOrderAsync(CreateOrderCommand cmd, CancellationToken ct)
+ {
+ // EN: Create root span for order processing
+ // VI: Tạo root span cho xử lý order
+ using var activity = ActivitySource.StartActivity(
+ "ProcessOrder",
+ ActivityKind.Internal);
+
+ activity?.SetTag("user.id", cmd.UserId);
+ activity?.SetTag("order.items_count", cmd.Items.Count);
+
+ try
+ {
+ // EN: Child span: Validate
+ // VI: Child span: Xác thực
+ using (var validateActivity = ActivitySource.StartActivity("ValidateOrder"))
+ {
+ await ValidateOrderAsync(cmd, ct);
+ validateActivity?.SetTag("validation.result", "success");
+ }
+
+ // EN: Child span: Check inventory
+ // VI: Child span: Kiểm tra tồn kho
+ using (var inventoryActivity = ActivitySource.StartActivity("CheckInventory"))
+ {
+ await CheckInventoryAsync(cmd.Items, ct);
+ }
+
+ // EN: Child span: Persist
+ // VI: Child span: Lưu trữ
+ Order order;
+ using (var persistActivity = ActivitySource.StartActivity("PersistOrder"))
+ {
+ order = await SaveOrderAsync(cmd, ct);
+ persistActivity?.SetTag("order.id", order.Id.ToString());
+ }
+
+ activity?.SetTag("order.id", order.Id.ToString());
+ activity?.SetTag("order.total", order.TotalAmount);
+ activity?.SetStatus(ActivityStatusCode.Ok);
+
+ return order;
+ }
+ catch (Exception ex)
+ {
+ activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
+ activity?.RecordException(ex);
+
+ _logger.LogError(ex, "Failed to process order for user {UserId}", cmd.UserId);
+ throw;
+ }
+ }
+}
+```
+
+---
+
+## Prometheus & Grafana
+
+### Docker Compose for Observability Stack
+
+```yaml
+# infra/observability/docker-compose.yml
+version: "3.8"
+
+services:
+ # ===================================
+ # PROMETHEUS
+ # ===================================
+ prometheus:
+ image: prom/prometheus:v2.47.0
+ container_name: prometheus
+ command:
+ - "--config.file=/etc/prometheus/prometheus.yml"
+ - "--storage.tsdb.path=/prometheus"
+ - "--web.enable-lifecycle"
+ volumes:
+ - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+ - ./prometheus/alerts:/etc/prometheus/alerts
+ - prometheus_data:/prometheus
+ ports:
+ - "9090:9090"
+ networks:
+ - goodgo-network
+
+ # ===================================
+ # GRAFANA
+ # ===================================
+ grafana:
+ image: grafana/grafana:10.1.0
+ container_name: grafana
+ environment:
+ - GF_SECURITY_ADMIN_USER=admin
+ - GF_SECURITY_ADMIN_PASSWORD=admin
+ - GF_USERS_ALLOW_SIGN_UP=false
+ volumes:
+ - ./grafana/provisioning:/etc/grafana/provisioning
+ - ./grafana/dashboards:/var/lib/grafana/dashboards
+ - grafana_data:/var/lib/grafana
+ ports:
+ - "3000:3000"
+ networks:
+ - goodgo-network
+ depends_on:
+ - prometheus
+ - loki
+
+ # ===================================
+ # LOKI (Log aggregation)
+ # ===================================
+ loki:
+ image: grafana/loki:2.9.0
+ container_name: loki
+ command: -config.file=/etc/loki/loki-config.yml
+ volumes:
+ - ./loki/loki-config.yml:/etc/loki/loki-config.yml
+ - loki_data:/loki
+ ports:
+ - "3100:3100"
+ networks:
+ - goodgo-network
+
+ # ===================================
+ # TEMPO (Distributed tracing)
+ # ===================================
+ tempo:
+ image: grafana/tempo:2.2.0
+ container_name: tempo
+ command: -config.file=/etc/tempo/tempo-config.yml
+ volumes:
+ - ./tempo/tempo-config.yml:/etc/tempo/tempo-config.yml
+ - tempo_data:/var/tempo
+ ports:
+ - "4317:4317" # OTLP gRPC
+ - "4318:4318" # OTLP HTTP
+ networks:
+ - goodgo-network
+
+volumes:
+ prometheus_data:
+ grafana_data:
+ loki_data:
+ tempo_data:
+
+networks:
+ goodgo-network:
+ external: true
+```
+
+### Prometheus Configuration
+
+```yaml
+# infra/observability/prometheus/prometheus.yml
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+alerting:
+ alertmanagers:
+ - static_configs:
+ - targets: []
+
+rule_files:
+ - /etc/prometheus/alerts/*.yml
+
+scrape_configs:
+ # EN: Prometheus self-monitoring
+ - job_name: "prometheus"
+ static_configs:
+ - targets: ["localhost:9090"]
+
+ # EN: GoodGo Services via Traefik
+ - job_name: "goodgo-services"
+ docker_sd_configs:
+ - host: unix:///var/run/docker.sock
+ filters:
+ - name: network
+ values: ["goodgo-network"]
+ relabel_configs:
+ - source_labels: [__meta_docker_container_name]
+ regex: /(.*)
+ target_label: container
+ - source_labels: [__meta_docker_container_label_com_docker_compose_service]
+ target_label: service
+ - source_labels: [__address__]
+ regex: (.+):.*
+ replacement: ${1}:8080
+ target_label: __address__
+ - source_labels: [__meta_docker_container_label_traefik_enable]
+ regex: "true"
+ action: keep
+
+ # EN: Traefik metrics
+ - job_name: "traefik"
+ static_configs:
+ - targets: ["traefik:8080"]
+```
+
+### Grafana Dashboard (JSON)
+
+```json
+{
+ "dashboard": {
+ "title": "GoodGo Services Overview",
+ "panels": [
+ {
+ "title": "Request Rate",
+ "type": "graph",
+ "targets": [
+ {
+ "expr": "rate(http_server_request_duration_seconds_count[5m])",
+ "legendFormat": "{{service}}"
+ }
+ ]
+ },
+ {
+ "title": "Error Rate",
+ "type": "graph",
+ "targets": [
+ {
+ "expr": "rate(http_server_request_duration_seconds_count{http_response_status_code=~\"5..\"}[5m])",
+ "legendFormat": "{{service}} - 5xx"
+ }
+ ]
+ },
+ {
+ "title": "Request Duration P99",
+ "type": "graph",
+ "targets": [
+ {
+ "expr": "histogram_quantile(0.99, rate(http_server_request_duration_seconds_bucket[5m]))",
+ "legendFormat": "{{service}}"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+---
+
+## Health Checks
+
+### Comprehensive Health Check Configuration
+
+```csharp
+///
+/// EN: Configure all health checks.
+/// VI: Cấu hình tất cả health checks.
+///
+
+builder.Services.AddHealthChecks()
+ // EN: Database
+ .AddNpgSql(
+ connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
+ name: "postgresql",
+ failureStatus: HealthStatus.Unhealthy,
+ tags: new[] { "db", "ready", "critical" })
+
+ // EN: Redis
+ .AddRedis(
+ redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
+ name: "redis",
+ failureStatus: HealthStatus.Degraded,
+ tags: new[] { "cache", "ready" })
+
+ // EN: External HTTP dependency
+ .AddUrlGroup(
+ new Uri(builder.Configuration["Services:Payment:HealthUrl"]!),
+ name: "payment-service",
+ failureStatus: HealthStatus.Degraded,
+ tags: new[] { "external", "ready" })
+
+ // EN: Disk space
+ .AddDiskStorageHealthCheck(
+ setup: options => options.AddDrive("/", 1024),
+ name: "disk-space",
+ failureStatus: HealthStatus.Degraded,
+ tags: new[] { "infrastructure" })
+
+ // EN: Memory
+ .AddProcessAllocatedMemoryHealthCheck(
+ maximumMegabytesAllocated: 500,
+ name: "memory",
+ tags: new[] { "infrastructure" });
+
+// EN: Map endpoints
+app.MapHealthChecks("/health/live", new HealthCheckOptions
+{
+ Predicate = _ => false,
+ ResponseWriter = WriteMinimalResponse
+});
+
+app.MapHealthChecks("/health/ready", new HealthCheckOptions
+{
+ Predicate = hc => hc.Tags.Contains("ready"),
+ ResponseWriter = WriteDetailedResponse
+});
+
+app.MapHealthChecks("/health", new HealthCheckOptions
+{
+ ResponseWriter = WriteDetailedResponse
+});
+
+// EN: Health check response writers
+static Task WriteMinimalResponse(HttpContext context, HealthReport report)
+{
+ context.Response.ContentType = "application/json";
+ return context.Response.WriteAsync(
+ JsonSerializer.Serialize(new { status = report.Status.ToString() }));
+}
+
+static Task WriteDetailedResponse(HttpContext context, HealthReport report)
+{
+ context.Response.ContentType = "application/json";
+
+ var result = new
+ {
+ status = report.Status.ToString(),
+ totalDuration = report.TotalDuration.TotalMilliseconds,
+ entries = report.Entries.Select(e => new
+ {
+ name = e.Key,
+ status = e.Value.Status.ToString(),
+ duration = e.Value.Duration.TotalMilliseconds,
+ description = e.Value.Description,
+ tags = e.Value.Tags,
+ data = e.Value.Data
+ })
+ };
+
+ return context.Response.WriteAsJsonAsync(result);
+}
+```
+
+---
+
+## Resources / Tài Nguyên
+
+- [OpenTelemetry .NET](https://opentelemetry.io/docs/instrumentation/net/)
+- [Serilog Documentation](https://serilog.net/)
+- [Prometheus Documentation](https://prometheus.io/docs/)
+- [Grafana Dashboards](https://grafana.com/grafana/dashboards/)
+- [Loki Documentation](https://grafana.com/docs/loki/)
diff --git a/.agent/skills/project-rules/SKILL.md b/.agent/skills/project-rules/SKILL.md
index 1baf937a..1c26b7bc 100644
--- a/.agent/skills/project-rules/SKILL.md
+++ b/.agent/skills/project-rules/SKILL.md
@@ -1,7 +1,7 @@
---
name: project-rules
description: GoodGo Platform coding standards and architecture. Use when working with services, apps, packages, or infrastructure.
-compatibility: ".NET 8+, ASP.NET Core, MediatR, Entity Framework Core"
+compatibility: ".NET 10+, ASP.NET Core, MediatR, Entity Framework Core"
metadata:
author: Velik Ho
version: "2.0"
diff --git a/.agent/skills/repository-pattern/SKILL.md b/.agent/skills/repository-pattern/SKILL.md
index 13cc0179..47c4b23a 100644
--- a/.agent/skills/repository-pattern/SKILL.md
+++ b/.agent/skills/repository-pattern/SKILL.md
@@ -1,7 +1,7 @@
---
name: repository-pattern
description: Entity Framework Core repository và data access patterns. Use for DbContext, repositories, migrations, aggregate roots, Unit of Work, và CQRS queries.
-compatibility: ".NET 8+, Entity Framework Core 8+, Dapper"
+compatibility: ".NET 10+, Entity Framework Core 8+, Dapper"
metadata:
author: Velik Ho
version: "1.0"
diff --git a/.agent/skills/security/SKILL.md b/.agent/skills/security/SKILL.md
index 482a82c7..e26956f6 100644
--- a/.agent/skills/security/SKILL.md
+++ b/.agent/skills/security/SKILL.md
@@ -1,7 +1,7 @@
---
name: security
description: Security patterns for GoodGo platform. Use for authentication, authorization, data protection, input validation, rate limiting, or secrets management.
-compatibility: ".NET 8+, ASP.NET Core Identity, Duende IdentityServer"
+compatibility: ".NET 10+, ASP.NET Core Identity, Duende IdentityServer"
metadata:
author: Velik Ho
version: "2.0"
diff --git a/.agent/skills/testing-patterns/SKILL.md b/.agent/skills/testing-patterns/SKILL.md
index 512f46d6..9cd40732 100644
--- a/.agent/skills/testing-patterns/SKILL.md
+++ b/.agent/skills/testing-patterns/SKILL.md
@@ -1,7 +1,7 @@
---
name: testing-patterns
description: Unit/Integration testing patterns cho .NET microservices. Use for xUnit, NSubstitute, Testcontainers, và testing MediatR handlers.
-compatibility: ".NET 8+, xUnit, NSubstitute, Testcontainers, Microsoft.AspNetCore.TestHost"
+compatibility: ".NET 10+, xUnit, NSubstitute, Testcontainers, Microsoft.AspNetCore.TestHost"
metadata:
author: Velik Ho
version: "1.0"