# Domain-Driven Design - Detailed Reference Detailed code examples cho DDD patterns trong GoodGo. ## Table of Contents 1. [Entity Patterns](#entity-patterns) 2. [Value Objects](#value-objects) 3. [Aggregates](#aggregates) 4. [Domain Events](#domain-events) 5. [Domain Services](#domain-services) 6. [Specifications](#specifications) --- ## Entity Patterns ### Complete Entity Base Class ```csharp /// /// EN: Base Entity class with domain events support. /// VI: Base Entity class với hỗ trợ domain events. /// public abstract class Entity : IEquatable { private readonly List _domainEvents = new(); private int? _requestedHashCode; public virtual Guid Id { get; protected set; } public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); protected void AddDomainEvent(IDomainEvent domainEvent) { _domainEvents.Add(domainEvent); } public void ClearDomainEvents() => _domainEvents.Clear(); public bool IsTransient() => Id == default; public bool Equals(Entity? other) { if (other is null) 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 bool Equals(object? obj) => Equals(obj as Entity); public override int GetHashCode() { if (IsTransient()) return base.GetHashCode(); _requestedHashCode ??= Id.GetHashCode() ^ 31; return _requestedHashCode.Value; } public static bool operator ==(Entity? left, Entity? right) => Equals(left, right); public static bool operator !=(Entity? left, Entity? right) => !Equals(left, right); } ``` ### Child Entity (OrderItem) ```csharp /// /// EN: OrderItem entity belonging to Order aggregate. /// VI: Entity OrderItem thuộc Order aggregate. /// public class OrderItem : Entity { public Guid ProductId { get; private set; } public string ProductName { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } public decimal TotalPrice => Quantity * UnitPrice; // EN: Required by EF Core private OrderItem() { } internal OrderItem(Guid productId, string productName, int quantity, decimal unitPrice) { Id = Guid.NewGuid(); ProductId = productId; ProductName = productName ?? throw new ArgumentNullException(nameof(productName)); SetQuantity(quantity); UnitPrice = unitPrice >= 0 ? unitPrice : throw new ArgumentException("Price cannot be negative"); } internal void SetQuantity(int quantity) { if (quantity <= 0) throw new DomainException("Quantity must be positive"); Quantity = quantity; } internal void IncreaseQuantity(int amount) { if (amount <= 0) throw new ArgumentException("Amount must be positive"); Quantity += amount; } } ``` --- ## Value Objects ### Value Object Base ```csharp /// /// EN: Base class for value objects with equality support. /// VI: Base class cho value objects với hỗ trợ equality. /// public abstract class ValueObject : IEquatable { protected abstract IEnumerable GetEqualityComponents(); public bool Equals(ValueObject? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; if (GetType() != other.GetType()) return false; return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } public override bool Equals(object? obj) => Equals(obj as ValueObject); public override int GetHashCode() { return GetEqualityComponents() .Aggregate(0, (hash, component) => HashCode.Combine(hash, component?.GetHashCode() ?? 0)); } public static bool operator ==(ValueObject? left, ValueObject? right) => Equals(left, right); public static bool operator !=(ValueObject? left, ValueObject? right) => !Equals(left, right); } ``` ### Complex Value Objects ```csharp /// /// EN: Email value object with validation. /// VI: Value object email với validation. /// public sealed class Email : ValueObject { public string Value { get; } private Email(string value) => Value = value; public static Email Create(string email) { if (string.IsNullOrWhiteSpace(email)) throw new DomainException("Email cannot be empty"); if (!IsValidEmail(email)) throw new DomainException("Invalid email format"); return new Email(email.ToLowerInvariant()); } private static bool IsValidEmail(string email) { try { var addr = new System.Net.Mail.MailAddress(email); return addr.Address == email; } catch { return false; } } protected override IEnumerable GetEqualityComponents() { yield return Value; } public override string ToString() => Value; public static implicit operator string(Email email) => email.Value; } /// /// EN: DateRange value object. /// VI: Value object khoảng thời gian. /// public sealed class DateRange : ValueObject { public DateTime Start { get; } public DateTime End { get; } public TimeSpan Duration => End - Start; private DateRange(DateTime start, DateTime end) { Start = start; End = end; } public static DateRange Create(DateTime start, DateTime end) { if (end < start) throw new DomainException("End date must be after start date"); return new DateRange(start, end); } public bool Contains(DateTime date) => date >= Start && date <= End; public bool Overlaps(DateRange other) => Start < other.End && End > other.Start; protected override IEnumerable GetEqualityComponents() { yield return Start; yield return End; } } /// /// EN: Money value object with currency operations. /// VI: Value object tiền tệ với các phép toán. /// public sealed class Money : ValueObject { public decimal Amount { get; } public Currency Currency { get; } private Money(decimal amount, Currency currency) { Amount = amount; Currency = currency; } public static Money Create(decimal amount, Currency currency) { if (amount < 0) throw new DomainException("Amount cannot be negative"); return new Money(amount, currency); } public static Money Zero(Currency currency) => new(0, currency); public Money Add(Money other) { EnsureSameCurrency(other); return new Money(Amount + other.Amount, Currency); } public Money Subtract(Money other) { EnsureSameCurrency(other); var result = Amount - other.Amount; if (result < 0) throw new DomainException("Insufficient funds"); return new Money(result, Currency); } public Money Multiply(decimal factor) { if (factor < 0) throw new ArgumentException("Factor cannot be negative"); return new Money(Amount * factor, Currency); } private void EnsureSameCurrency(Money other) { if (Currency != other.Currency) throw new DomainException("Cannot perform operation on different currencies"); } protected override IEnumerable GetEqualityComponents() { yield return Amount; yield return Currency; } public override string ToString() => $"{Amount:N2} {Currency}"; } public enum Currency { VND, USD, EUR } ``` --- ## Aggregates ### Complete Aggregate Example ```csharp /// /// EN: Order aggregate with full business logic. /// VI: Order aggregate với business logic đầy đủ. /// public class Order : Entity, IAggregateRoot { private readonly List _items = new(); public string UserId { get; private set; } = default!; public Address ShippingAddress { get; private set; } = default!; public Address? BillingAddress { get; private set; } public OrderStatus Status { get; private set; } public Money TotalAmount { get; private set; } = Money.Zero(Currency.VND); public string? CancellationReason { get; private set; } public DateTime CreatedAt { get; private set; } public DateTime? SubmittedAt { get; private set; } public DateTime? ShippedAt { get; private set; } public DateTime? DeliveredAt { get; private set; } public IReadOnlyCollection Items => _items.AsReadOnly(); private Order() { } // EF Core public Order(string userId, Address shippingAddress, Address? billingAddress = null) { Id = Guid.NewGuid(); UserId = userId ?? throw new ArgumentNullException(nameof(userId)); ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress)); BillingAddress = billingAddress; Status = OrderStatus.Draft; CreatedAt = DateTime.UtcNow; AddDomainEvent(new OrderCreatedDomainEvent(Id, userId)); } // EN: Command methods / VI: Các method command public void AddItem(Guid productId, string productName, int quantity, decimal unitPrice) { EnsureOrderIsDraft(); var existingItem = _items.FirstOrDefault(i => i.ProductId == productId); if (existingItem != null) { existingItem.IncreaseQuantity(quantity); } else { var item = new OrderItem(productId, productName, quantity, unitPrice); _items.Add(item); } RecalculateTotal(); AddDomainEvent(new OrderItemAddedDomainEvent(Id, productId, quantity)); } public void UpdateItemQuantity(Guid productId, int newQuantity) { EnsureOrderIsDraft(); var item = _items.FirstOrDefault(i => i.ProductId == productId) ?? throw new DomainException($"Item {productId} not found in order"); if (newQuantity <= 0) { _items.Remove(item); } else { item.SetQuantity(newQuantity); } RecalculateTotal(); } public void UpdateShippingAddress(Address newAddress) { if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered) throw new DomainException("Cannot change address for shipped order"); ShippingAddress = newAddress ?? throw new ArgumentNullException(nameof(newAddress)); } public void Submit() { EnsureOrderIsDraft(); if (!_items.Any()) throw new DomainException("Cannot submit empty order"); Status = OrderStatus.Submitted; SubmittedAt = DateTime.UtcNow; AddDomainEvent(new OrderSubmittedDomainEvent(Id, UserId, TotalAmount.Amount)); } public void ConfirmPayment() { if (Status != OrderStatus.Submitted) throw new DomainException($"Cannot confirm payment for order in {Status} status"); Status = OrderStatus.Paid; AddDomainEvent(new OrderPaidDomainEvent(Id, TotalAmount.Amount)); } public void Ship(string trackingNumber) { if (Status != OrderStatus.Paid) throw new DomainException("Order must be paid before shipping"); Status = OrderStatus.Shipped; ShippedAt = DateTime.UtcNow; AddDomainEvent(new OrderShippedDomainEvent(Id, trackingNumber)); } public void Cancel(string reason) { if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered) throw new DomainException("Cannot cancel shipped order"); Status = OrderStatus.Cancelled; CancellationReason = reason; AddDomainEvent(new OrderCancelledDomainEvent(Id, reason)); } // EN: Private helpers / VI: Helpers private private void EnsureOrderIsDraft() { if (Status != OrderStatus.Draft) throw new DomainException("Order is not in draft status"); } private void RecalculateTotal() { var total = _items.Sum(i => i.TotalPrice); TotalAmount = Money.Create(total, Currency.VND); } } public enum OrderStatus { Draft, Submitted, Paid, Shipped, Delivered, Cancelled } ``` --- ## Domain Events ### Domain Event Definitions ```csharp /// /// EN: Domain events for Order aggregate. /// VI: Domain events cho Order aggregate. /// public record OrderCreatedDomainEvent(Guid OrderId, string UserId) : IDomainEvent { public Guid Id { get; } = Guid.NewGuid(); public DateTime OccurredOn { get; } = DateTime.UtcNow; } public record OrderItemAddedDomainEvent(Guid OrderId, Guid ProductId, int Quantity) : IDomainEvent { public Guid Id { get; } = Guid.NewGuid(); public DateTime OccurredOn { get; } = DateTime.UtcNow; } public record OrderSubmittedDomainEvent(Guid OrderId, string UserId, decimal TotalAmount) : IDomainEvent { public Guid Id { get; } = Guid.NewGuid(); public DateTime OccurredOn { get; } = DateTime.UtcNow; } ``` ### Domain Event Dispatcher ```csharp /// /// EN: Dispatch domain events via MediatR. /// VI: Dispatch domain events qua MediatR. /// public class DomainEventDispatcher : IDomainEventDispatcher { private readonly IMediator _mediator; private readonly ILogger _logger; public async Task DispatchEventsAsync(IEnumerable entities, CancellationToken ct = default) { var domainEvents = entities .SelectMany(e => e.DomainEvents) .ToList(); foreach (var domainEvent in domainEvents) { _logger.LogDebug( "Dispatching domain event {EventType}: {EventId}", domainEvent.GetType().Name, domainEvent.Id); await _mediator.Publish(domainEvent, ct); } foreach (var entity in entities) { entity.ClearDomainEvents(); } } } ``` --- ## Resources / Tài Nguyên - [Domain-Driven Design - Eric Evans](https://www.domainlanguage.com/) - [Implementing DDD - Vaughn Vernon](https://www.informit.com/store/implementing-domain-driven-design-9780321834577) - [Microsoft DDD Guidance](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)