dotnet-ddd
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDomain-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 skill.
domain-analysis基于现代C#的战术DDD实现模式——使用Entities、Value Objects、Aggregates、Domain Events和Repositories构建丰富的领域模型。
适用范围:本技能涵盖战术DDD(核心构建块)。如需战略DDD相关内容(Bounded Contexts、Context Mapping、子域分析),请使用技能。
domain-analysisWhen 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
核心概念
| Concept | What It Is | C# Implementation |
|---|---|---|
| Entity | Object with identity that persists across state changes | Class with |
| Value Object | Immutable object defined by its attributes, no identity | |
| Aggregate | Cluster of Entities/VOs with a single root, consistency boundary | Root entity that guards all invariants |
| Aggregate Root | Entry point to an Aggregate — the only externally-referenced entity | Public API, owns child entities |
| Domain Event | Something that happened in the domain that other parts care about | |
| Repository | Abstraction for persisting/retrieving Aggregates | Interface in Domain, implementation in Infrastructure |
| Domain Service | Stateless operation that doesn't belong to a single Entity/VO | Static method or injected service |
| Specification | Encapsulated query/business rule | Class with |
| 概念 | 定义 | C#实现方式 |
|---|---|---|
| Entity | 具备唯一标识,且标识在状态变更中保持不变的对象 | 包含 |
| Value Object | 无唯一标识,由属性值定义的不可变对象 | 使用 |
| Aggregate | 由多个Entities/Value Objects组成的集群,以单个根实体为核心,维护一致性边界 | 由根实体管控所有不变量的实体集群 |
| Aggregate Root | Aggregate的入口点——唯一允许外部直接引用的实体 | 对外暴露公共API,拥有子实体的控制权 |
| Domain Event | 领域中发生的、其他模块需要感知的事件 | 实现 |
| Repository | 用于持久化/检索Aggregates的抽象层 | 在领域层定义接口,在基础设施层实现 |
| Domain Service | 不属于单个Entity/VO的无状态操作 | 静态方法或依赖注入的服务 |
| Specification | 封装查询逻辑或业务规则的对象 | 包含 |
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使用record
实现Value Object
recordcsharp
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:
- Reference other Aggregates by ID only, never by direct object reference
- All state changes go through the Aggregate Root
- One transaction = one Aggregate (eventual consistency between Aggregates)
- 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规则:
- 仅通过ID引用其他Aggregate,禁止直接引用对象
- 所有状态变更必须通过Aggregate Root发起
- 一个事务仅操作一个Aggregate(Aggregates之间通过最终一致性保证数据一致)
- 保持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) instead of public constructors for AggregatesFrom - Raise Domain Events for side effects that cross Aggregate boundaries
- Use on all async Repository methods
CancellationToken
- 领域层不得依赖基础设施(禁止引用EF、HTTP、日志等组件)
- 对无标识概念(如Money、Address、Email)使用Value Objects
- 在Aggregate内部强制维护不变量——禁止外部修改
- 仅通过ID引用其他Aggregate
- 使用工厂方法(如、
Create)替代公共构造函数创建AggregateFrom - 当发生跨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 ——解决方案布局、层依赖关系、项目引用