其他
在 .NET 中实现 DDD 战术模式时,用聚合、值对象…
适用于 .NET 应用中需要按 DDD 战术模式实现复杂业务规则、聚合一致性、副作用解耦和聚合持久化的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 58/100 ×0.15
适用场景
在 .NET 中实现 DDD 战术模式时特定
解决思路
- 用聚合定义一致性边界:把实体和值对象组织为一次数据变更的单一单元;在单个事务中强制聚合内所有不变量;跨聚合一致性采用最终一致。
- 用值对象替代原始类型:为 Money、EmailAddress、OrderNumber 等创建不可变值对象,使其携带校验、相等性和行为;C# 中用 sealed record 实现不可变值对象。
- 用领域事件解耦副作用:当领域中有意义的事情发生(OrderPlaced、PaymentReceived)时抛领域事件;发邮件、更新读模型、通知其他聚合等副作用订阅这些事件;聚合只关注自身规则。
- 聚合根作为唯一入口:外部代码只通过根实体访问聚合;子实体绝不独立加载或修改;根实体强制整个聚合的所有不变量。
- 仓储持久化聚合而非实体:每个聚合根一个 repository;repository 以整体加载和保存整个聚合;不为子实体创建 repository;Infrastructure 实现内部使用 DbContext,但它是聚合边界的 DDD 战术模式,不是通用 CRUD 包装。
- 实现 Aggregate Root:创建 sealed class 继承 AggregateRoot;子集合用 private readonly List<...>;提供 EF Core 用的 private 无参构造;属性使用 private set;暴露 IReadOnlyList 投影,如 _lines.AsReadOnly();用静态工厂如 Place 创建,并用 Guid.CreateVersion7() 生成 Id,同时 RaiseDomainEvent;提供行为方法如 AddLine、Confirm,返回 Result 并校验状态、数量和空行;AddLine 对已有行调用 IncreaseQuantity,否则添加 new OrderLine;变更后 RecalculateTotal;不暴露公共 setter。
- 实现 Value Objects as Records:用 sealed record;构造函数校验,如 ArgumentOutOfRangeException.ThrowIfNegative、ArgumentException.ThrowIfNullOrWhiteSpace;规范化如 ToUpperInvariant;提供静态工厂如 Zero;重载运算符如 + 并校验币种一致;其他 EmailAddress、OrderNumber 等同模式,保持 sealed record、构造校验、无公共 setter。
- 实现强类型 ID:为跨边界的聚合根 ID 创建 readonly record struct,如 CustomerId(Guid Value)、ProductId(Guid Value)、OrderNumber(string Value);提供 New() 并使用 Guid.CreateVersion7();重写 ToString。
- 配置 EF Core 转换器:在 IEntityTypeConfiguration 中配置 HasKey;对强类型 ID 使用 HasConversion(id => id.Value, value => new CustomerId(value));对 OrderNumber 同样转换并 HasMaxLength(50);对 Money 使用 ComplexProperty 配置 Amount 列名为 Total、精度 (18, 2),Currency 列名 Currency、最大长度 3;配置 HasMany(o => o.Lines).WithOne().HasForeignKey("OrderId");配置 Navigation(o => o.Lines).AutoInclude()。
- 派发领域事件:AggregateRoot 维护 private List<IDomainEvent>,暴露 IReadOnlyList,使用 protected RaiseDomainEvent 添加,使用 ClearDomainEvents 清空;IDomainEvent 继承 INotification(来自 MIT 许可的 Mediator 包,不是 MediatR)或使用普通标记接口;事件用 record 实现 IDomainEvent 并提供 OccurredAt;AppDbContext 通过主构造函数注入 IPublisher;重写 SaveChangesAsync,先从 ChangeTracker 找出有 DomainEvents 的 AggregateRoot,收集事件,执行 base.SaveChangesAsync,再逐个 publisher.Publish,最后 ClearDomainEvents。
- 实现 Domain Services:当逻辑不属于单个聚合时创建领域服务,如 PricingService;注入领域接口如 IDiscountPolicy;方法接收 ProductId、quantity、unitPrice、CustomerId,返回值对象 Money;协调跨聚合逻辑,不把聚合不应知道的外部数据塞进聚合。
- 避免 Oversized Aggregates:不要在一个聚合根下拥有 Orders、Payments、Cart 等;把 Customer 缩减为 Name、Email 等自身属性,Orders、Payments、Cart 作为独立聚合并通过 CustomerId 引用。
- 避免对聚合内逻辑使用领域事件:同一聚合内不要 RaiseDomainEvent 后再由 handler 重算;直接调用私有方法,如 _lines.Add(line) 后 RecalculateTotal()。
- 避免值对象带身份:值对象不要有 Id;用基于属性的 record,如 Address(string Street, string City, string PostalCode, string Country)。
- 避免贫血聚合:聚合不要只是带 public setter 的数据袋;不要让服务直接 order.Status = OrderStatus.Confirmed 或 order.Lines.Add(newLine);应调用 order.Confirm()、order.AddLine(productId, quantity, unitPrice) 等封装行为,由聚合校验并抛事件。
- 按决策规则选择 DDD:复杂领域且业务规则超出 CRUD 时才用 DDD;简单 CRUD、设置、审计日志、读模型用普通实体。
- 按决策规则使用值对象:任何有校验规则或相等性基于属性而非身份的概念都使用值对象。
- 按决策规则控制聚合大小:保持小,通常 1 个根实体 + 0-3 个子实体;每次加载整个聚合。
- 按决策规则区分事件:领域事件用于限界上下文内、同一事务;集成事件用于跨上下文、经消息总线。
- 按决策规则使用强类型 ID:跨边界的聚合根 ID 始终用强类型;子实体 ID 可选。
- 按决策规则选择持久化方式:复杂聚合每个聚合根一个 repository;更简单的查询使用 IAppDbContext。
- 按决策规则使用领域服务:仅在逻辑需要多个聚合或聚合不应知道的外部数据时使用领域服务。
适用边界 · 注意事项
- 简单 CRUD、设置、审计日志、读模型等没有复杂业务规则的场景,不适用 DDD,应使用普通实体。
- 仅在同一聚合内部完成的逻辑,不适用领域事件,应直接调用聚合自身方法。
- 当逻辑需要多个聚合或外部数据时,不适用把逻辑硬塞进单个聚合或聚合内事件,应使用领域服务。