dotnet-ddd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Domain-Driven Design in .NET

在.NET中实现领域驱动设计(DDD)

Tactical DDD implementation patterns in modern C# — building rich domain models with Entities, Value Objects, Aggregates, Domain Events, and Repositories.
Scope: This skill covers tactical DDD (the building blocks). For strategic DDD (Bounded Contexts, Context Mapping, subdomain analysis), use the
domain-analysis
skill.
基于现代C#的战术DDD实现模式——使用Entities、Value Objects、Aggregates、Domain Events和Repositories构建丰富的领域模型。
适用范围:本技能涵盖战术DDD(核心构建块)。如需战略DDD相关内容(Bounded Contexts、Context Mapping、子域分析),请使用
domain-analysis
技能。

When to Use

适用场景

  • Modeling a domain with complex business rules
  • Implementing Entities, Value Objects, or Aggregates
  • Raising and handling Domain Events
  • Designing Repository interfaces
  • Structuring a .NET solution with DDD layers
  • Applying the Result pattern, Strongly-typed IDs, or Specification pattern
Not for: Simple CRUD apps, anemic domain models, or when business logic lives entirely in services.
  • 为包含复杂业务规则的领域建模
  • 实现Entities、Value Objects或Aggregates
  • 发布与处理Domain Events
  • 设计Repository接口
  • 采用DDD分层架构搭建.NET解决方案
  • 应用Result模式、强类型ID或Specification模式
不适用场景:简单CRUD应用、贫血领域模型,或业务逻辑完全位于服务层的场景。

Key Concepts

核心概念

ConceptWhat It IsC# Implementation
EntityObject with identity that persists across state changesClass with
Id
, equality by identity
Value ObjectImmutable object defined by its attributes, no identity
record
or
sealed class
with structural equality
AggregateCluster of Entities/VOs with a single root, consistency boundaryRoot entity that guards all invariants
Aggregate RootEntry point to an Aggregate — the only externally-referenced entityPublic API, owns child entities
Domain EventSomething that happened in the domain that other parts care about
record
implementing
IDomainEvent
RepositoryAbstraction for persisting/retrieving AggregatesInterface in Domain, implementation in Infrastructure
Domain ServiceStateless operation that doesn't belong to a single Entity/VOStatic method or injected service
SpecificationEncapsulated query/business ruleClass with
IsSatisfiedBy(T)
概念定义C#实现方式
Entity具备唯一标识,且标识在状态变更中保持不变的对象包含
Id
属性的类,基于标识判断相等性
Value Object无唯一标识,由属性值定义的不可变对象使用
record
sealed class
,基于结构判断相等性
Aggregate由多个Entities/Value Objects组成的集群,以单个根实体为核心,维护一致性边界由根实体管控所有不变量的实体集群
Aggregate RootAggregate的入口点——唯一允许外部直接引用的实体对外暴露公共API,拥有子实体的控制权
Domain Event领域中发生的、其他模块需要感知的事件实现
IDomainEvent
接口的
record
Repository用于持久化/检索Aggregates的抽象层在领域层定义接口,在基础设施层实现
Domain Service不属于单个Entity/VO的无状态操作静态方法或依赖注入的服务
Specification封装查询逻辑或业务规则的对象包含
IsSatisfiedBy(T)
方法的类

Entity Base Class

Entity基类

csharp
public abstract class Entity<TId> : IEquatable<Entity<TId>>
    where TId : notnull
{
    public TId Id { get; protected init; }

    private readonly List<IDomainEvent> _domainEvents = [];
    public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents;

    protected Entity(TId id) => Id = id;

    public void RaiseDomainEvent(IDomainEvent domainEvent) =>
        _domainEvents.Add(domainEvent);

    public void ClearDomainEvents() => _domainEvents.Clear();

    public bool Equals(Entity<TId>? other) =>
        other is not null && Id.Equals(other.Id);

    public override bool Equals(object? obj) =>
        obj is Entity<TId> other && Equals(other);

    public override int GetHashCode() => Id.GetHashCode();

    public static bool operator ==(Entity<TId>? left, Entity<TId>? right) =>
        Equals(left, right);

    public static bool operator !=(Entity<TId>? left, Entity<TId>? right) =>
        !Equals(left, right);

    // Protected parameterless constructor for ORM
    protected Entity() => Id = default!;
}
csharp
public abstract class Entity<TId> : IEquatable<Entity<TId>>
    where TId : notnull
{
    public TId Id { get; protected init; }

    private readonly List<IDomainEvent> _domainEvents = [];
    public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents;

    protected Entity(TId id) => Id = id;

    public void RaiseDomainEvent(IDomainEvent domainEvent) =>
        _domainEvents.Add(domainEvent);

    public void ClearDomainEvents() => _domainEvents.Clear();

    public bool Equals(Entity<TId>? other) =>
        other is not null && Id.Equals(other.Id);

    public override bool Equals(object? obj) =>
        obj is Entity<TId> other && Equals(other);

    public override int GetHashCode() => Id.GetHashCode();

    public static bool operator ==(Entity<TId>? left, Entity<TId>? right) =>
        Equals(left, right);

    public static bool operator !=(Entity<TId>? left, Entity<TId>? right) =>
        !Equals(left, right);

    // 为ORM提供的无参构造函数
    protected Entity() => Id = default!;
}

Value Object with
record

使用
record
实现Value Object

csharp
public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException($"Cannot add {Currency} to {other.Currency}");
        return this with { Amount = Amount + other.Amount };
    }

    public static Money Zero(string currency) => new(0, currency);
}

public record Address(string Street, string City, string State, string ZipCode, string Country);

public record DateRange
{
    public DateOnly Start { get; init; }
    public DateOnly End { get; init; }

    public DateRange(DateOnly start, DateOnly end)
    {
        if (end < start)
            throw new ArgumentException("End date must be after start date");
        Start = start;
        End = end;
    }

    public bool Overlaps(DateRange other) =>
        Start <= other.End && other.Start <= End;
}
csharp
public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException($"Cannot add {Currency} to {other.Currency}");
        return this with { Amount = Amount + other.Amount };
    }

    public static Money Zero(string currency) => new(0, currency);
}

public record Address(string Street, string City, string State, string ZipCode, string Country);

public record DateRange
{
    public DateOnly Start { get; init; }
    public DateOnly End { get; init; }

    public DateRange(DateOnly start, DateOnly end)
    {
        if (end < start)
            throw new ArgumentException("End date must be after start date");
        Start = start;
        End = end;
    }

    public bool Overlaps(DateRange other) =>
        Start <= other.End && other.Start <= End;
}

Aggregate Example

Aggregate示例

csharp
public sealed class Order : Entity<OrderId>
{
    private readonly List<OrderLine> _lines = [];
    public IReadOnlyList<OrderLine> Lines => _lines;
    public CustomerId CustomerId { get; private init; }
    public OrderStatus Status { get; private set; }
    public Money Total => _lines.Aggregate(Money.Zero("USD"), (sum, line) => sum.Add(line.SubTotal));

    private Order() { } // ORM

    public static Order Create(CustomerId customerId)
    {
        var order = new Order(OrderId.New())
        {
            CustomerId = customerId,
            Status = OrderStatus.Draft
        };
        order.RaiseDomainEvent(new OrderCreatedEvent(order.Id));
        return order;
    }

    public void AddLine(ProductId productId, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Can only add lines to draft orders");
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");

        var line = new OrderLine(OrderLineId.New(), productId, quantity, unitPrice);
        _lines.Add(line);
    }

    public void Submit()
    {
        if (_lines.Count == 0)
            throw new DomainException("Cannot submit an empty order");
        Status = OrderStatus.Submitted;
        RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
    }
}
Aggregate rules:
  1. Reference other Aggregates by ID only, never by direct object reference
  2. All state changes go through the Aggregate Root
  3. One transaction = one Aggregate (eventual consistency between Aggregates)
  4. Keep Aggregates small — only include what must be immediately consistent
csharp
public sealed class Order : Entity<OrderId>
{
    private readonly List<OrderLine> _lines = [];
    public IReadOnlyList<OrderLine> Lines => _lines;
    public CustomerId CustomerId { get; private init; }
    public OrderStatus Status { get; private set; }
    public Money Total => _lines.Aggregate(Money.Zero("USD"), (sum, line) => sum.Add(line.SubTotal));

    private Order() { } // 供ORM使用

    public static Order Create(CustomerId customerId)
    {
        var order = new Order(OrderId.New())
        {
            CustomerId = customerId,
            Status = OrderStatus.Draft
        };
        order.RaiseDomainEvent(new OrderCreatedEvent(order.Id));
        return order;
    }

    public void AddLine(ProductId productId, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Can only add lines to draft orders");
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");

        var line = new OrderLine(OrderLineId.New(), productId, quantity, unitPrice);
        _lines.Add(line);
    }

    public void Submit()
    {
        if (_lines.Count == 0)
            throw new DomainException("Cannot submit an empty order");
        Status = OrderStatus.Submitted;
        RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
    }
}
Aggregate规则:
  1. 仅通过ID引用其他Aggregate,禁止直接引用对象
  2. 所有状态变更必须通过Aggregate Root发起
  3. 一个事务仅操作一个Aggregate(Aggregates之间通过最终一致性保证数据一致)
  4. 保持Aggregate轻量化——仅包含必须强一致性的内容

Strongly-Typed IDs

强类型ID

csharp
public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString();
}

public readonly record struct CustomerId(Guid Value)
{
    public static CustomerId New() => new(Guid.NewGuid());
}
csharp
public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString();
}

public readonly record struct CustomerId(Guid Value)
{
    public static CustomerId New() => new(Guid.NewGuid());
}

Domain Events

领域事件

csharp
public interface IDomainEvent
{
    DateTime OccurredOn { get; }
}

public abstract record DomainEvent : IDomainEvent
{
    public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}

public record OrderCreatedEvent(OrderId OrderId) : DomainEvent;
public record OrderSubmittedEvent(OrderId OrderId, Money Total) : DomainEvent;
csharp
public interface IDomainEvent
{
    DateTime OccurredOn { get; }
}

public abstract record DomainEvent : IDomainEvent
{
    public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}

public record OrderCreatedEvent(OrderId OrderId) : DomainEvent;
public record OrderSubmittedEvent(OrderId OrderId, Money Total) : DomainEvent;

Repository Interface

Repository接口

csharp
// Define in Domain layer — implement in Infrastructure
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
    Task AddAsync(Order order, CancellationToken ct = default);
    Task UpdateAsync(Order order, CancellationToken ct = default);
}

// Optional: generic base interface
public interface IRepository<T, TId>
    where T : Entity<TId>
    where TId : notnull
{
    Task<T?> GetByIdAsync(TId id, CancellationToken ct = default);
    Task AddAsync(T entity, CancellationToken ct = default);
}
csharp
// 在领域层定义接口——在基础设施层实现
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
    Task AddAsync(Order order, CancellationToken ct = default);
    Task UpdateAsync(Order order, CancellationToken ct = default);
}

// 可选:通用基类接口
public interface IRepository<T, TId>
    where T : Entity<TId>
    where TId : notnull
{
    Task<T?> GetByIdAsync(TId id, CancellationToken ct = default);
    Task AddAsync(T entity, CancellationToken ct = default);
}

Result Pattern (No Exceptions for Expected Failures)

Result模式(预期失败场景不抛出异常)

csharp
public sealed class Result<T>
{
    public T? Value { get; }
    public Error? Error { get; }
    public bool IsSuccess => Error is null;

    private Result(T value) => Value = value;
    private Result(Error error) => Error = error;

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(Error error) => new(error);

    public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<Error, TOut> onFailure) =>
        IsSuccess ? onSuccess(Value!) : onFailure(Error!);
}

public record Error(string Code, string Message);
Usage in aggregate:
csharp
public Result<Order> Submit()
{
    if (_lines.Count == 0)
        return Result<Order>.Failure(OrderErrors.EmptyOrder);
    Status = OrderStatus.Submitted;
    RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
    return Result<Order>.Success(this);
}
csharp
public sealed class Result<T>
{
    public T? Value { get; }
    public Error? Error { get; }
    public bool IsSuccess => Error is null;

    private Result(T value) => Value = value;
    private Result(Error error) => Error = error;

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(Error error) => new(error);

    public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<Error, TOut> onFailure) =>
        IsSuccess ? onSuccess(Value!) : onFailure(Error!);
}

public record Error(string Code, string Message);
在Aggregate中的使用示例:
csharp
public Result<Order> Submit()
{
    if (_lines.Count == 0)
        return Result<Order>.Failure(OrderErrors.EmptyOrder);
    Status = OrderStatus.Submitted;
    RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
    return Result<Order>.Success(this);
}

Constraints

约束规则

MUST DO

必须遵守

  • Keep domain layer free of infrastructure dependencies (no EF, no HTTP, no logging)
  • Use Value Objects for concepts with no identity (Money, Address, Email)
  • Enforce invariants inside the Aggregate — never outside
  • Reference other Aggregates by ID only
  • Use factory methods (
    Create
    ,
    From
    ) instead of public constructors for Aggregates
  • Raise Domain Events for side effects that cross Aggregate boundaries
  • Use
    CancellationToken
    on all async Repository methods
  • 领域层不得依赖基础设施(禁止引用EF、HTTP、日志等组件)
  • 对无标识概念(如Money、Address、Email)使用Value Objects
  • 在Aggregate内部强制维护不变量——禁止外部修改
  • 仅通过ID引用其他Aggregate
  • 使用工厂方法(如
    Create
    From
    )替代公共构造函数创建Aggregate
  • 当发生跨Aggregate边界的副作用时,发布Domain Events
  • 所有异步Repository方法需使用
    CancellationToken

MUST NOT DO

禁止操作

  • Expose setters on Aggregate state (use behavior methods instead)
  • Let Aggregates depend on repositories or services
  • Create "God Aggregates" that contain everything
  • Use Domain Events for intra-Aggregate communication
  • Put business logic in Application Services — it belongs in the domain
  • Use anemic domain models (entities as data bags with logic in services)
  • 暴露Aggregate状态的setter方法(改用行为方法)
  • 让Aggregate依赖仓储或服务
  • 创建包含所有内容的"上帝Aggregate"
  • 使用Domain Events进行Aggregate内部通信
  • 将业务逻辑放在应用服务中——业务逻辑属于领域层
  • 使用贫血领域模型(实体仅作为数据容器,逻辑放在服务中)

Additional References

额外参考资料

Load based on your task — do not load all at once:
  • references/building-blocks.md — deep dive into Entity, Value Object, Aggregate patterns with edge cases
  • references/domain-events.md — event dispatching strategies, outbox pattern, integration events
  • references/patterns.md — Result pattern, Specification, Strongly-typed IDs, Guard clauses
  • references/project-structure.md — solution layout, layer dependencies, project references
根据任务需求加载——请勿一次性加载全部
  • references/building-blocks.md ——深入讲解Entity、Value Object、Aggregate模式及边界案例
  • references/domain-events.md ——事件分发策略、Outbox模式、集成事件
  • references/patterns.md ——Result模式、Specification、强类型ID、守卫子句
  • references/project-structure.md ——解决方案布局、层依赖关系、项目引用

Resources

资源链接