# 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
/// <summary>
/// EN: Base Entity class with domain events support.
/// VI: Base Entity class với hỗ trợ domain events.
/// </summary>
public abstract class Entity : IEquatable<Entity>
{
    private readonly List<IDomainEvent> _domainEvents = new();
    private int? _requestedHashCode;

    public virtual Guid Id { get; protected set; }

    public IReadOnlyCollection<IDomainEvent> 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
/// <summary>
/// EN: OrderItem entity belonging to Order aggregate.
/// VI: Entity OrderItem thuộc Order aggregate.
/// </summary>
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
/// <summary>
/// EN: Base class for value objects with equality support.
/// VI: Base class cho value objects với hỗ trợ equality.
/// </summary>
public abstract class ValueObject : IEquatable<ValueObject>
{
    protected abstract IEnumerable<object?> 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
/// <summary>
/// EN: Email value object with validation.
/// VI: Value object email với validation.
/// </summary>
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<object?> GetEqualityComponents()
    {
        yield return Value;
    }

    public override string ToString() => Value;
    
    public static implicit operator string(Email email) => email.Value;
}

/// <summary>
/// EN: DateRange value object.
/// VI: Value object khoảng thời gian.
/// </summary>
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<object?> GetEqualityComponents()
    {
        yield return Start;
        yield return End;
    }
}

/// <summary>
/// EN: Money value object with currency operations.
/// VI: Value object tiền tệ với các phép toán.
/// </summary>
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<object?> 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
/// <summary>
/// EN: Order aggregate with full business logic.
/// VI: Order aggregate với business logic đầy đủ.
/// </summary>
public class Order : Entity, IAggregateRoot
{
    private readonly List<OrderItem> _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<OrderItem> 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
/// <summary>
/// EN: Domain events for Order aggregate.
/// VI: Domain events cho Order aggregate.
/// </summary>
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
/// <summary>
/// EN: Dispatch domain events via MediatR.
/// VI: Dispatch domain events qua MediatR.
/// </summary>
public class DomainEventDispatcher : IDomainEventDispatcher
{
    private readonly IMediator _mediator;
    private readonly ILogger<DomainEventDispatcher> _logger;

    public async Task DispatchEventsAsync(IEnumerable<Entity> 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/)
