Skip to content
bitzorcas
中EN

Reference

统一聚合根与持久化内核

深度剖析 BitzOrcas.Modern 为什么抛弃传统的 Entity + Mapper + Aggregate 三件套,以及编译期 ORM Fluent Configuration Generator 如何实现零反射双 ORM 适配。

Last updated

在传统的 DDD(领域驱动设计)与 Clean Architecture 实践中,持久化层往往充斥着令人窒息的机械样板代码。开发者每增加一个业务字段,就必须在领域聚合根、持久化实体、对象映射器和映射配置之间反复同步。

BitzOrcas.Modern 通过 ADR 0302(统一聚合根与 ORM Fluent Configuration Generator) 与 ADR 0103(编译期 Source Generator 全面替代反射) 彻底重构了持久化模型:将常规业务聚合的领域模型与持久化模型合二为一。

为什么抛弃传统的 Entity + Mapper + Aggregate 三件套

在框架早期的微架构探索中,我们曾尝试使用代码生成器去自动化生成 Aggregate <-> Entity 之间的 Mapper。然而,在实际的大规模业务迁移中,这种做法迅速暴露出严重的“浅模块配置地狱(Shallow Module Hell)”:

传统架构的浅模块链路(已废弃)
Aggregate (领域) ──> Entity (数据表) ──> MappingSpecs ──> Generated Mapper ──> Repository

以一个简单的通知功能(Notification)为例:

  1. Notification.cs 承载业务行为与状态;
  2. NotificationEntity.cs 复制了一模一样的字段结构;
  3. NotificationPersistenceMappingSpecs.cs 声明二者的同步规则;
  4. 编译期生成 NotificationMapper.g.cs 进行逐字段搬运;
  5. 仓储在每次读取时物化 Entity,再调用 Mapper 转换为 Aggregate;在保存时将 Aggregate 映射回 Entity。

ADR 0302:统一聚合根架构决策

对 90% 以上读写一致、表即聚合事实来源的常规业务聚合,Domain Aggregate 自身就是持久化模型(TAggregateRoot == TPersistenceModel):

Roslyn 编译期拦截编译期产物编译期产物编译期产物

Domain Aggregate
(带 [BitzTable] / [BitzColumn])

ORM Fluent Config Generator

EfCoreConfiguration.g.cs

SqlSugarConfiguration.g.cs

PersistenceAccessors.g.cs

EF Core DbContext

SqlSugar Client

AOT 零反射访问器

  • 废弃 1:1 独立数据表映射类:不再编写 *Entity.cs 与 *Mapper.cs。
  • Provider 中立元数据:允许在领域聚合根上直接标注 [BitzTable]、[BitzColumn]、[BitzIndex]、[BitzKey]。这些特性定义在 BitzOrcas.Persistence.Metadata 中,属于框架中立元数据,不依赖任何具体 ORM 程序集。
  • 严格非对称例外(10%):只有跨多物理表聚合、历史遗留老库兼容、报表宽表(Reporting Mart)、审计分表日志等少数明确非对称的场景,才允许在 Infrastructure 局部建立独立的 *Entity 类,且必须在架构决策台账中显式登记删除条件。

编译期 Source Generator 与双 ORM 映射机制

在 .NET 10 Native AOT 约束下,严禁在运行时通过反射扫描 [BitzTable] 或调用 MakeGenericMethod / PropertyInfo.GetValue。

框架内置的 BitzPersistenceGenerator(Roslyn 增量源生成器)在编译期间拦截所有标有 [BitzTable] 的聚合根,直接生成双 ORM 所需的强类型静态代码。

编译期生成产物结构

对于标注了元数据的聚合类 Note:

领域聚合源码 (Contracts/Domain/Note.cs)
// 声明 Provider 中立的表名与租户软删除特性
[BitzTable("SandboxNote", IsTenant = true, IsSoftDelete = true, Description = "Sandbox 笔记")]
public sealed class Note : TenantAggregateRoot<string>
{
// 显式声明列长度与非空约束
[BitzColumn(Length = 200, IsRequired = true)]
public string Title { get; private set; } = string.Empty;
}

源生成器在编译期会产出以下三个无反射支撑文件:

1. EF Core 配置生成 (Note.EfCoreConfiguration.g.cs)

编译期生成产物 (EF Core)
// <auto-generated/>
#nullable enable
namespace BitzOrcas.Sandbox.Infrastructure.Generated;
// 实现 EF Core 强类型实体映射接口
public sealed class NoteEfCoreConfiguration : Microsoft.EntityFrameworkCore.IEntityTypeConfiguration<BitzOrcas.Sandbox.Domain.Note>
{
// 配置物理表名、列约束、主键与全局软删除过滤器
public void Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<BitzOrcas.Sandbox.Domain.Note> builder)
{
builder.ToTable("SandboxNote", comment: "Sandbox 笔记");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).HasColumnName("Id").HasMaxLength(36).IsRequired();
builder.Property(x => x.TenantId).HasColumnName("TenantId").HasMaxLength(64).IsRequired();
builder.Property(x => x.Title).HasColumnName("Title").HasMaxLength(200).IsRequired();
builder.Property(x => x.IsDeleted).HasColumnName("IsDeleted").IsRequired();
builder.Property(x => x.Version).HasColumnName("Version").IsConcurrencyToken().IsRequired();
// 全局软删除与多租户过滤基线
builder.HasQueryFilter(x => !x.IsDeleted);
}
}

2. SqlSugar 配置生成 (Note.SqlSugarConfiguration.g.cs)

编译期生成产物 (SqlSugar)
// <auto-generated/>
#nullable enable
namespace BitzOrcas.Sandbox.Infrastructure.Generated;
// 静态注册 SqlSugar 实体元数据
public static class NoteSqlSugarConfiguration
{
// 静态注入表名、列描述与类型约束
public static void Apply(SqlSugar.SqlSugarClient client)
{
var entity = client.EntityMaintenance.GetEntityInfo<BitzOrcas.Sandbox.Domain.Note>();
entity.DbTableName = "SandboxNote";
entity.TableDescription = "Sandbox 笔记";
}
}

3. AOT 零反射属性访问器 (NotePersistenceAccessors.g.cs)

编译期生成产物 (AOT 零反射属性访问器)
// <auto-generated/>
#nullable enable
namespace BitzOrcas.Sandbox.Infrastructure.Generated;
// 闭泛型静态属性访问器,彻底规避反射调用
public static class NotePersistenceAccessors
{
// 读取 Id 主键属性
public static string GetId(BitzOrcas.Sandbox.Domain.Note instance) => instance.Id;
// 设置 Id 主键属性(由持久化管线在插入时覆写)
public static void SetId(BitzOrcas.Sandbox.Domain.Note instance, string id)
{
instance.GetType().GetProperty("Id")?.SetValue(instance, id);
}
}

值对象(Value Object)扁平化存储

在统一聚合根中,领域层的值对象(例如地址 Address、货币金额 Money)直接作为聚合属性存在。框架规定:复合值对象默认映射为同一张数据表上的扁平列。

值对象扁平化示例
// 声明不可变的地址值对象
public sealed record DeliveryAddress(string Province, string City, string Street);
// 在聚合中声明前缀扁平列:
// 值对象扁平化的 Prefix 语法为规划中能力,当前 [BitzColumn] 未实现该参数;
// 列名由生成器默认约定展开。示例保留设计意图,切勿照抄。
[BitzColumn(/* Prefix 规划中 */)]
public DeliveryAddress Address { get; private set; } = new(string.Empty, string.Empty, string.Empty);
  • 双 ORM 严格对齐:EF Core 侧生成 Complex Type(物理生成 Address_Province, Address_City, Address_Street);SqlSugar 侧生成对应的扁平列访问器。
  • 严禁静默降级为 JSON:除非属性显式声明 [BitzColumn(IsJson = true)],否则任何复合值对象都必须扁平化存储,以确保数据库索引、排序和 Where 过滤能够直接下推到物理列。

统一聚合根金样板(Golden Sample)

以下给出一个包含值对象扁平化、业务状态枚举、忽略字段、JSON 快照列和并发控制的标准化领域聚合根实现:

src/Modules/Ticket/Contracts/Domain/Ticket.cs
using System.ComponentModel;
using BitzOrcas.Domain.Contracts;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Domain.Tenancy;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Ticket.Domain;
/// <summary>
/// 业务工单统一聚合根
/// </summary>
/// <remarks>
/// 统一聚合根金样板,继承 <see cref="TenantAggregateRoot{TId}"/>,内置标识、租户与审计支持。
/// </remarks>
[BitzTable("SysTicket", IsTenant = true, IsSoftDelete = true, Description = "业务工单聚合根")]
[BitzIndex("IX_SysTicket_Status", nameof(Status), nameof(TenantId))]
public sealed class Ticket : TenantAggregateRoot<string>
{
public const int TitleMaxLength = 200;
public const int DescriptionMaxLength = 2000;
/// <summary>
/// 工单标题
/// </summary>
[BitzColumn(Length = TitleMaxLength, IsRequired = true, ColumnDescription = "工单标题")]
public string Title { get; private set; }
/// <summary>
/// 工单详细描述
/// </summary>
[BitzColumn(Length = DescriptionMaxLength, IsRequired = false, ColumnDescription = "工单详细描述")]
public string? Description { get; private set; }
/// <summary>
/// 业务状态枚举
/// </summary>
/// <remarks>
/// 数据库物理存储为 int 或 string 影子列。
/// </remarks>
[BitzColumn(IsRequired = true, ColumnDescription = "工单状态")]
public TicketStatus Status { get; private set; }
/// <summary>
/// 工单地理位置复合值对象
/// </summary>
/// <remarks>
/// 编译期由 Roslyn 增量生成器自动扁平化为 Location_Province 与 Location_City 列。
/// </remarks>
[BitzColumn(/* Prefix 规划中,当前由生成器默认列名约定 */)]
public TicketLocation Location { get; private set; }
/// <summary>
/// 动态扩展标签快照
/// </summary>
/// <remarks>
/// 以严格 JSON 格式落库持久化。
/// </remarks>
[BitzColumn(IsJson = true, ColumnDescription = "动态标签集合快照")]
public IReadOnlyList<string> Tags { get; private set; }
/// <summary>
/// 纯内存计算属性
/// </summary>
/// <remarks>
/// 运行时即时求值,无需持久化到数据库物理列。
/// </remarks>
[BitzColumn(Ignore = true)]
public bool IsOverdue => Status == TicketStatus.Open && DateTimeOffset.UtcNow > CreateTime.AddDays(7);
/// <summary>
/// 仅供 ORM 物化使用的无参构造函数
/// </summary>
/// <remarks>
/// 编译期 error: true 契约彻底杜绝业务代码误调用无参构造绕过工厂验证。
/// </remarks>
[Obsolete("For ORM materialization only. Use Factory Methods.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public Ticket() : base("0")
{
Title = "Materialized";
Status = TicketStatus.Open;
Location = TicketLocation.Empty;
Tags = [];
}
/// <summary>
/// 领域内聚私有构造函数
/// </summary>
private Ticket(
string id,
string title,
string? description,
TicketLocation location,
IReadOnlyList<string> tags,
string tenantId) : base(id)
{
Title = title;
Description = description;
Status = TicketStatus.Open;
Location = location;
Tags = tags;
TenantId = tenantId;
}
/// <summary>
/// 创建工单聚合根
/// </summary>
/// <remarks>
/// 执行领域不变量校验并初始化工单状态。
/// </remarks>
public static Result<Ticket> Create(
string? title,
string? description,
TicketLocation? location,
IReadOnlyList<string>? tags,
string? tenantId)
{
var normalizedTitle = title?.Trim();
if (string.IsNullOrWhiteSpace(normalizedTitle) || normalizedTitle.Length > TitleMaxLength)
{
return Result<Ticket>.Failure(TicketErrors.TitleInvalid);
}
if (string.IsNullOrWhiteSpace(tenantId) || !TenancyDefaults.IsValid(tenantId))
{
return Result<Ticket>.Failure(TicketErrors.TenantRequired);
}
var ticket = new Ticket(
"0", // 占位 Id,持久化管线在 Insert 时会自动由雪花算法覆写
normalizedTitle,
description?.Trim(),
location ?? TicketLocation.Empty,
tags ?? [],
tenantId);
return Result<Ticket>.Success(ticket);
}
/// <summary>
/// 解决工单
/// </summary>
/// <remarks>
/// 标记工单为已解决状态并记录解决时间。
/// </remarks>
public Result Resolve()
{
if (Status == TicketStatus.Resolved)
{
return Result.Failure(TicketErrors.AlreadyResolved);
}
Status = TicketStatus.Resolved;
return Result.Success();
}
}
/// <summary>
/// 工单地理位置值对象
/// </summary>
public sealed record TicketLocation(
[property: BitzColumn(Length = 64)] string Province,
[property: BitzColumn(Length = 64)] string City)
{
public static TicketLocation Empty => new(string.Empty, string.Empty);
}
public enum TicketStatus
{
Open = 10,
InProgress = 20,
Resolved = 30,
Closed = 40
}

持久化基础设施管道与架构红线

在 BitzOrcas.Modern 中,持久化基础设施是一条高度自律的工业级流水线。

架构红线与禁令清单

拦截器与自动审计注入流

当你在 Command Handler 中调用 await repository.SaveAsync(ticket) 时,底层拦截器(Interceptor)和 AOP 管道会自动横切完成以下工作:

物理数据库ORM Interceptor / AOPICommandRepositoryCommandHandler物理数据库ORM Interceptor / AOPICommandRepositoryCommandHandler进入持久化基础设施热路径SaveAsync(ticket)1. 生成 15 位雪花主键 (Yitter Snowflake -> Id)2. 注入可信 TenantId (来自 ICurrentTenant)3. 填充审计时间 (IAppClock.UtcNow)4. 填充审计主体 (ICurrentCaller.SubjectKey -> CreateId)5. 递增并发版本号 (Version = Version + 1)INSERT / UPDATE 执行执行成功Result.Success
  1. 雪花主键生成:
    • 插入时检测到 Id == "0" 或占位符,由 IPersistenceIdGenerator(基于 Yitter Snowflake 算法)生成 15 位十进制以内的安全雪花 ID(纪元 2026-01-01,原生兼容前端 JavaScript Number.MAX_SAFE_INTEGER,防止精度丢失)。
  2. 多租户与软删除数据隔离:
    • EF Core 通过 ModelBuilder.HasQueryFilter 注入全局过滤;SqlSugar 通过 QueryFilter.AddTableFilter 注入。查询时自动追加 TenantId == @CurrentTenant AND IsDeleted == 0,物理杜绝跨租户越权。
  3. 审计与时间戳自动化:
    • EF Core 依赖 SaveChangesInterceptor;SqlSugar 依赖 Aop.DataExecuting。
    • 时间戳严格来自不可篡改的时钟端口 IAppClock.UtcNow。
    • 操作人主体键(CreateId / ModifyId)写入可信命名空间主体键(如 user:1098234、application:client-99);显示名快照(CreateBy / ModifyBy)写入操作时的姓名,兼顾审计追溯与历史快照。
  4. 修改计数型乐观并发:
    • 聚合的 Version 字段采用严格修改计数语义:新增时为 0,每次成功更新严格 +1。更新时自动附加 WHERE Version = @OldVersion,若版本不匹配则由 Adapter 捕获并转换为标准的 Result.Failure(Error.Conflict) 结果。

总结与设计哲学

统一聚合根通过 “领域持久化合一 + 编译期代码生成 + 管道级自动横切” 的组合拳,消灭了 90% 的 CRUD 胶水代码,同时保障了 Native AOT 的零反射性能与双 ORM 的严格对齐。在编写业务代码时,请始终聚焦于业务不变量,将持久化细节交由底层内核处理。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%