Mục tiêu: Thiết kế hệ thống với Clean Architecture và Domain-Driven Design, áp dụng tactical patterns trong .NET.
1. Clean Architecture Layers
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (Controllers, Minimal APIs, gRPC) │
│ Depends on → Application Layer │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ (EF Core, External APIs, Email, Storage) │
│ Implements → Application Interfaces │
├─────────────────────────────────────────────────────────────┤
│ Application Layer │
│ (Use Cases, Commands, Queries, DTOs) │
│ Depends on → Domain Layer │
├─────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ (Entities, Value Objects, Domain Events) │
│ KHÔNG depend vào bất kỳ layer nào! │
└─────────────────────────────────────────────────────────────┘
Dependency Rule: ngoài → trong (KHÔNG BAO GIỜ ngược lại)
Project Structure
src/
├── MyApp.Domain/ ← Trung tâm, 0 dependencies
│ ├── Entities/
│ │ ├── Order.cs
│ │ └── Product.cs
│ ├── ValueObjects/
│ │ ├── Money.cs
│ │ └── Address.cs
│ ├── Events/
│ │ └── OrderCreatedEvent.cs
│ ├── Enums/
│ │ └── OrderStatus.cs
│ ├── Exceptions/
│ │ └── DomainException.cs
│ └── Interfaces/
│ └── IOrderRepository.cs ← Interface ONLY
│
├── MyApp.Application/ ← Use cases, orchestration
│ ├── Orders/
│ │ ├── Commands/
│ │ │ ├── CreateOrder/
│ │ │ │ ├── CreateOrderCommand.cs
│ │ │ │ ├── CreateOrderCommandHandler.cs
│ │ │ │ └── CreateOrderCommandValidator.cs
│ │ │ └── CancelOrder/
│ │ └── Queries/
│ │ └── GetOrderById/
│ ├── Common/
│ │ ├── Interfaces/
│ │ │ ├── IUnitOfWork.cs
│ │ │ └── ICurrentUserService.cs
│ │ └── Behaviors/
│ │ ├── ValidationBehavior.cs
│ │ └── LoggingBehavior.cs
│ └── DependencyInjection.cs
│
├── MyApp.Infrastructure/ ← Implementations
│ ├── Persistence/
│ │ ├── AppDbContext.cs
│ │ ├── Configurations/
│ │ └── Repositories/
│ │ └── OrderRepository.cs
│ ├── Services/
│ │ ├── EmailService.cs
│ │ └── PaymentService.cs
│ └── DependencyInjection.cs
│
└── MyApp.Api/ ← Presentation
├── Controllers/
├── Middleware/
└── Program.cs
2. Domain Layer - Rich Domain Model
Entity Base Class
csharppublic abstract class Entity
{
public Guid Id { get; protected set; }
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void RaiseDomainEvent(IDomainEvent domainEvent)
=> _domainEvents.Add(domainEvent);
public void ClearDomainEvents() => _domainEvents.Clear();
}
public abstract class AggregateRoot : Entity
{
// Aggregate Root = entry point cho một nhóm entities
// Bên ngoài CHỈ truy cập qua Aggregate Root
}
Rich Domain Model - Order Example
csharppublic class Order : AggregateRoot
{
private readonly List<OrderItem> _items = [];
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public Money Total { get; private set; } = null!;
public Address ShippingAddress { get; private set; } = null!;
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public DateTime CreatedAt { get; private set; }
private Order() { } // EF Core
// Factory method - encapsulate creation logic
public static Order Create(Guid customerId, Address shippingAddress)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
ShippingAddress = shippingAddress,
Status = OrderStatus.Draft,
Total = Money.Zero("VND"),
CreatedAt = DateTime.UtcNow
};
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id, customerId));
return order;
}
// Business logic TRONG entity, không phải service
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Can only add items to draft orders");
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existingItem is not null)
{
existingItem.UpdateQuantity(existingItem.Quantity + quantity);
}
else
{
_items.Add(OrderItem.Create(product.Id, product.Name, product.Price, quantity));
}
RecalculateTotal();
}
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be submitted");
if (_items.Count == 0)
throw new DomainException("Order must have at least one item");
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
}
public void Cancel(string reason)
{
if (Status is OrderStatus.Shipped or OrderStatus.Delivered)
throw new DomainException("Cannot cancel shipped/delivered orders");
Status = OrderStatus.Cancelled;
RaiseDomainEvent(new OrderCancelledEvent(Id, reason));
}
private void RecalculateTotal()
{
Total = _items.Aggregate(
Money.Zero(Total.Currency),
(sum, item) => sum + item.LineTotal);
}
}
Value Objects
csharppublic record Money
{
public decimal Amount { get; init; }
public string Currency { get; init; }
public Money(decimal amount, string currency)
{
if (amount < 0) throw new DomainException("Amount cannot be negative");
if (string.IsNullOrWhiteSpace(currency)) throw new DomainException("Currency required");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
public static Money Zero(string currency) => new(0, currency);
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new DomainException($"Cannot add {a.Currency} and {b.Currency}");
return new Money(a.Amount + b.Amount, a.Currency);
}
public static Money operator *(Money money, int multiplier)
=> new(money.Amount * multiplier, money.Currency);
}
Domain Events
csharppublic interface IDomainEvent
{
DateTime OccurredOn { get; }
}
public record OrderCreatedEvent(Guid OrderId, Guid CustomerId) : IDomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
// Dispatch domain events trong SaveChanges
public class AppDbContext : DbContext
{
private readonly IMediator _mediator;
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
// Collect domain events trước khi save
var events = ChangeTracker.Entries<Entity>()
.SelectMany(e => e.Entity.DomainEvents)
.ToList();
// Save to database first
var result = await base.SaveChangesAsync(ct);
// Dispatch events sau khi save thành công
foreach (var domainEvent in events)
{
await _mediator.Publish(domainEvent, ct);
}
// Clear events
ChangeTracker.Entries<Entity>()
.ToList()
.ForEach(e => e.Entity.ClearDomainEvents());
return result;
}
}
3. Rich Domain Model vs Anemic Domain Model
csharp// ❌ Anemic Domain Model - entity chỉ có properties
public class Order
{
public Guid Id { get; set; }
public OrderStatus Status { get; set; } // Public setter = ai cũng sửa được
public decimal Total { get; set; }
}
// Business logic nằm trong Service (procedural style)
public class OrderService
{
public void SubmitOrder(Order order)
{
if (order.Status != OrderStatus.Draft) throw new Exception("..");
order.Status = OrderStatus.Submitted; // Violate encapsulation!
}
}
// ✅ Rich Domain Model - entity chứa behavior
public class Order
{
public OrderStatus Status { get; private set; } // Private setter!
public void Submit()
{
// Business rule gắn liền với entity
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be submitted");
Status = OrderStatus.Submitted;
}
}
// Service chỉ orchestrate, KHÔNG chứa business logic
public class SubmitOrderHandler
{
public async Task Handle(SubmitOrderCommand cmd)
{
var order = await _repo.GetByIdAsync(cmd.OrderId);
order.Submit(); // Entity tự validate
await _repo.SaveAsync();
}
}
4. Bounded Contexts & Context Mapping
┌─────────────────────────────────────────────────────────────────┐
│ eCommerce System │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Ordering │ │ Catalog │ │ Shipping │ │
│ │ Context │ │ Context │ │ Context │ │
│ │ │ │ │ │ │ │
│ │ Order │ │ Product │ │ Shipment │ │
│ │ OrderItem │ │ Category │ │ Route │ │
│ │ Customer ←──┼───┼→ ProductRef │ │ Tracking │ │
│ │ (ID only!) │ │ (full data) │ │ OrderRef │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │ │ │ │
│ └────── Integration Events ──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Key: Mỗi context có model RIÊNG cho cùng 1 concept
- Ordering: Customer = CustomerId + Name (enough to place order)
- Shipping: Customer = Address + Phone (enough to ship)
- Catalog: Customer = KHÔNG CÓ (không liên quan)
Tổng Kết Session 12
| Concept | Key Takeaway |
|---|---|
| Clean Architecture | Dependencies hướng vào trong, Domain không depend gì |
| Rich Domain Model | Business logic trong Entity, không trong Service |
| Value Objects | Immutable, equality by value (dùng record) |
| Aggregate Root | Entry point, đảm bảo consistency |
| Domain Events | Decouple side effects, dispatch sau SaveChanges |
| Bounded Context | Mỗi context có model riêng, communicate qua events |
Bài tập: Refactor 1 Anemic Domain Model (entity chỉ có properties) thành Rich Domain Model: encapsulate business rules, private setters, factory methods, domain events.