18 KiB
18 KiB
Technical Reference: .NET Microservice Workflow
Chi tiết kỹ thuật cho từng giai đoạn phát triển .NET Microservices.
Phase 1: Domain Layer Technical Details
1.1 Entity Base Class
/// <summary>
/// EN: Base class for all entities with domain events support.
/// VI: Base class cho entities với hỗ trợ domain events.
/// </summary>
public abstract class Entity
{
private int? _requestedHashCode;
private List<IDomainEvent>? _domainEvents;
public virtual Guid Id { get; protected set; }
public IReadOnlyCollection<IDomainEvent> DomainEvents
=> _domainEvents?.AsReadOnly() ?? Array.Empty<IDomainEvent>().AsReadOnly();
public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents ??= new List<IDomainEvent>();
_domainEvents.Add(eventItem);
}
public void ClearDomainEvents() => _domainEvents?.Clear();
public bool IsTransient() => Id == default;
public override bool Equals(object? obj)
{
if (obj is not Entity other) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
if (IsTransient() || other.IsTransient()) return false;
return Id.Equals(other.Id);
}
public override int GetHashCode()
{
if (!IsTransient())
{
_requestedHashCode ??= Id.GetHashCode() ^ 31;
return _requestedHashCode.Value;
}
return base.GetHashCode();
}
}
1.2 Aggregate Root Interface
/// <summary>
/// EN: Marker interface for aggregate roots.
/// VI: Interface đánh dấu aggregate roots.
/// </summary>
public interface IAggregateRoot { }
1.3 Value Object Base Class
/// <summary>
/// EN: Base class for value objects.
/// VI: Base class cho value objects.
/// </summary>
public abstract class ValueObject
{
protected abstract IEnumerable<object?> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj is null || obj.GetType() != GetType())
return false;
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject? left, ValueObject? right)
=> left?.Equals(right) ?? right is null;
public static bool operator !=(ValueObject? left, ValueObject? right)
=> !(left == right);
}
1.4 Domain Event Interface
/// <summary>
/// EN: Domain event interface, integrates with MediatR.
/// VI: Interface domain event, tích hợp với MediatR.
/// </summary>
public interface IDomainEvent : INotification
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
1.5 Domain Exception
/// <summary>
/// EN: Exception for domain rule violations.
/// VI: Exception cho vi phạm quy tắc domain.
/// </summary>
public class DomainException : Exception
{
public DomainException(string message) : base(message) { }
public DomainException(string message, Exception innerException)
: base(message, innerException) { }
}
Phase 2: Infrastructure Layer Technical Details
2.1 Repository Interface
/// <summary>
/// EN: Base repository interface for aggregate roots.
/// VI: Interface repository cơ bản cho aggregate roots.
/// </summary>
public interface IRepository<T> where T : class, IAggregateRoot
{
IUnitOfWork UnitOfWork { get; }
Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<T> AddAsync(T entity, CancellationToken ct = default);
void Update(T entity);
void Delete(T entity);
}
2.2 Unit of Work Interface
/// <summary>
/// EN: Unit of Work interface - DbContext implements this.
/// VI: Interface Unit of Work - DbContext implement interface này.
/// </summary>
public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync(CancellationToken ct = default);
}
2.3 EF Core Configuration Example
/// <summary>
/// EN: EF Core configuration for Order aggregate.
/// VI: Cấu hình EF Core cho Order aggregate.
/// </summary>
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(50);
builder.Property(o => o.TotalAmount)
.HasPrecision(18, 2);
// EN: Configure owned entity (Value Object)
// VI: Cấu hình owned entity (Value Object)
builder.OwnsOne(o => o.ShippingAddress, a =>
{
a.Property(p => p.Street).HasMaxLength(200);
a.Property(p => p.City).HasMaxLength(100);
a.Property(p => p.PostalCode).HasMaxLength(20);
});
// EN: Configure child entities
// VI: Cấu hình entities con
builder.HasMany(o => o.OrderItems)
.WithOne()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade);
// EN: Use backing field for collection
// VI: Dùng backing field cho collection
builder.Navigation(o => o.OrderItems)
.UsePropertyAccessMode(PropertyAccessMode.Field);
}
}
2.4 Repository Implementation
/// <summary>
/// EN: EF Core repository implementation.
/// VI: Triển khai repository với EF Core.
/// </summary>
public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public IUnitOfWork UnitOfWork => _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
return await _context.Orders.FindAsync(new object[] { id }, ct);
}
public async Task<Order?> GetWithItemsAsync(Guid id, CancellationToken ct = default)
{
return await _context.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o => o.Id == id, ct);
}
public async Task<Order> AddAsync(Order order, CancellationToken ct = default)
{
var entry = await _context.Orders.AddAsync(order, ct);
return entry.Entity;
}
public void Update(Order order)
{
_context.Entry(order).State = EntityState.Modified;
}
public void Delete(Order order)
{
_context.Orders.Remove(order);
}
}
Phase 3: Application Layer Technical Details
3.1 Command Definition
/// <summary>
/// EN: Command to create a new order.
/// VI: Command tạo order mới.
/// </summary>
public record CreateOrderCommand(
string UserId,
Address ShippingAddress,
List<OrderItemDto> Items) : IRequest<OrderResult>;
public record OrderResult(Guid OrderId);
3.2 Command Handler
/// <summary>
/// EN: Handler for CreateOrderCommand.
/// VI: Handler cho CreateOrderCommand.
/// </summary>
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, OrderResult>
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<CreateOrderCommandHandler> _logger;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<OrderResult> 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);
}
}
3.3 Query with Dapper
/// <summary>
/// EN: Query handler using Dapper for optimized reads.
/// VI: Query handler dùng Dapper cho đọc tối ưu.
/// </summary>
public class GetUserOrdersQueryHandler
: IRequestHandler<GetUserOrdersQuery, PagedResult<OrderSummaryDto>>
{
private readonly IDbConnection _connection;
public GetUserOrdersQueryHandler(IDbConnection connection)
{
_connection = connection;
}
public async Task<PagedResult<OrderSummaryDto>> Handle(
GetUserOrdersQuery request,
CancellationToken ct)
{
const string countSql = "SELECT COUNT(*) FROM Orders WHERE UserId = @UserId";
var total = await _connection.ExecuteScalarAsync<int>(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<OrderSummaryDto>(sql, new
{
request.UserId,
request.Skip,
request.Take
});
return new PagedResult<OrderSummaryDto>(orders.ToList(), total);
}
}
3.4 Validation Behavior
/// <summary>
/// EN: Pipeline behavior for FluentValidation.
/// VI: 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 ct)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(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();
}
}
Phase 4: API Layer Technical Details
4.1 Slim Controller
/// <summary>
/// EN: Slim controller using MediatR.
/// VI: Controller gọn nhẹ với MediatR.
/// </summary>
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/orders")]
[SwaggerTag("Order Management")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// EN: Create a new order.
/// VI: Tạo order mới.
/// </summary>
[HttpPost]
[SwaggerOperation(Summary = "Create order")]
[SwaggerResponse(201, "Created")]
[SwaggerResponse(400, "Validation error")]
public async Task<ActionResult<ApiResponse<OrderResult>>> 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<OrderResult>.Ok(result));
}
private string GetUserId() =>
User.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException();
}
4.2 Health Checks
/// <summary>
/// EN: Health check configuration in Program.cs.
/// VI: Cấu hình health check trong Program.cs.
/// </summary>
// EN: Register health checks
// VI: Đăng ký health checks
builder.Services.AddHealthChecks()
.AddNpgSql(connectionString, name: "postgresql")
.AddRedis(redisConnectionString, name: "redis")
.AddRabbitMQ(rabbitConnectionString, name: "rabbitmq");
// EN: Map health check endpoint
// VI: Map endpoint health check
app.MapHealthChecks("/hc", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
4.3 Resilience with Polly
/// <summary>
/// EN: Configure HTTP client with Polly resilience.
/// VI: Cấu hình HTTP client với Polly resilience.
/// </summary>
builder.Services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
client.BaseAddress = new Uri(catalogServiceUrl);
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryAttempt, context) =>
{
// Log retry attempt
});
}
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));
}
4.4 DI Registration
/// <summary>
/// EN: DI registration in Program.cs.
/// VI: Đăng ký DI trong Program.cs.
/// </summary>
// 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);
// EN: Register repositories
// VI: Đăng ký repositories
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// EN: Register DbContext
// VI: Đăng ký DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));
// EN: Register Dapper connection for queries
// VI: Đăng ký Dapper connection cho queries
builder.Services.AddScoped<IDbConnection>(_ =>
new NpgsqlConnection(connectionString));
Anti-Patterns to Avoid
1. Anemic Domain Model
// ❌ AVOID: No behavior, just data
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
// Then in service:
order.Status = "Submitted"; // No validation!
2. God Service
// ❌ AVOID: All logic in one service
public class OrderService
{
public void CreateOrder() { /* 500 lines */ }
public void UpdateOrder() { /* 300 lines */ }
public void ProcessPayment() { /* 400 lines */ }
// ...
}
3. Direct Database Access in Controller
// ❌ AVOID: DbContext in controller
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
var order = new Order { ... };
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return Ok();
}
4. Shared Database Between Services
// ❌ AVOID: Multiple services accessing same database
// OrderService -> SharedDb.Orders
// InventoryService -> SharedDb.Orders (reading Order table!)
File Structure Summary
src/
├── ServiceName.API/
│ ├── Controllers/
│ │ └── OrdersController.cs
│ ├── Application/
│ │ ├── Commands/
│ │ │ ├── CreateOrderCommand.cs
│ │ │ └── CreateOrderCommandHandler.cs
│ │ ├── Queries/
│ │ │ ├── GetOrderQuery.cs
│ │ │ └── GetOrderQueryHandler.cs
│ │ ├── Validators/
│ │ │ └── CreateOrderCommandValidator.cs
│ │ └── Behaviors/
│ │ ├── LoggingBehavior.cs
│ │ └── ValidationBehavior.cs
│ └── Program.cs
├── ServiceName.Domain/
│ ├── AggregatesModel/
│ │ └── OrderAggregate/
│ │ ├── Order.cs
│ │ ├── OrderItem.cs
│ │ ├── OrderStatus.cs
│ │ └── IOrderRepository.cs
│ ├── Events/
│ │ └── OrderCreatedDomainEvent.cs
│ ├── Exceptions/
│ │ └── DomainException.cs
│ └── SeedWork/
│ ├── Entity.cs
│ ├── ValueObject.cs
│ └── IAggregateRoot.cs
└── ServiceName.Infrastructure/
├── Data/
│ ├── ApplicationDbContext.cs
│ └── Configurations/
│ └── OrderConfiguration.cs
└── Repositories/
└── OrderRepository.cs