Skip to content
bitzorcas
中EN

Reference

核心构建块

Entity、AggregateRoot、TenantAggregateRoot、Result、Error、领域事件、查询值对象、平台硬边界与时钟的当前契约。

Last updated

BitzOrcas.Domain 是最内层的共享构建块。它不注册 DI、不启动中间件,也不引用 ORM。业务模块使用这里的类型表达身份、状态边界、失败语义和领域事实。

实体与聚合层级

Entity<TId>
├─ Id
├─ CreateTime / ModifyTime / CreateBy / ModifyBy / ImpersonatorId
└─ DomainEvents + Raise() + ClearDomainEvents()
│
▼
AggregateRoot<TId>
├─ IsDeleted / DeleteTime / DeleteBy
└─ Version
├─ TenantAggregateRoot<TId> + TenantId
└─ PlatformAggregateRoot<TId>

Entity<TId>

实体以具体类型和 Id 判断相等。基类已经实现 IAuditableEntity,并收集待分发领域事件。新对象使用占位标识时,持久化管线可以通过 AssignPersistenceId 回填最终标识。

不要在业务代码中随意设置审计属性。它们是持久化与调用者上下文共同维护的横切状态。

AggregateRoot<TId>

聚合根是一致性和事务边界。它在实体基础上实现:

  • ISoftDelete:IsDeleted、DeleteTime、DeleteBy;
  • IConcurrencyTracked:乐观并发 Version。

外部代码通过聚合根行为改变内部状态,不应直接持有并修改聚合内部实体。

租户与平台聚合

租户业务数据继承 TenantAggregateRoot<TId>,得到字符串 TenantId;平台级数据继承 PlatformAggregateRoot<TId>。选择哪一种是所有权决定,不是为了少写一个属性。

请求中的租户值不可信。聚合创建时应使用当前用户、系统作业或经验证的租户作用域。

Result 与 Error

预期的业务失败不抛异常,而是返回 Result 或 Result<T>:

// ① 先看契约与控制流;校验、取消和类型化错误都要显式保留。
public static Result<Note> Create(string title, string tenantId)
{
// ② 聚合工厂保护不变量,并用稳定 Error 说明预期失败。
if (string.IsNullOrWhiteSpace(title))
{
return Result<Note>.Failure(SandboxErrors.NoteTitleInvalid);
}
return Result<Note>.Success(new Note(title, tenantId));
}

Error 包含稳定 Code、开发诊断 Description 和 ErrorType。当前错误类型及默认 HTTP 语义为:

ErrorType典型含义HTTP
Validation格式、必填、长度、枚举范围400
NotFound资源不存在404
Conflict并发、幂等或状态冲突409
Forbidden已认证但无权限403
Unauthorized未认证401
Failure用例级业务规则不满足422
Unexpected未预期系统失败的兜底500

不存在 Infrastructure 错误类型。数据库不可用等未预期故障进入异常与 Problem Details 通道,除非某个外部调用本身有明确、可恢复的业务失败契约。

Description 不是面向用户的本地化文案。API 或客户端通过稳定错误码映射显示文本。

领域事件

领域事件是聚合已经发生的事实:

// ① 先看契约与控制流;校验、取消和类型化错误都要显式保留。
[IntegrationTopic("note.created")]
public sealed record NoteCreatedDomainEvent(
string TenantId,
string Title) : DomainEvent;

聚合调用 Raise(event) 收集事件,Application 使用 IDomainEventCollector 跟踪聚合,DomainEventDispatchPipelineBehavior 按事务约定分发。带 [IntegrationTopic] 的事件进入集成发布路径。

注意边界:

  • 事件使用过去时命名;
  • 只携带消费者需要的稳定事实,不携带 ORM 实体;
  • 聚合构造或状态变化负责抛出事件,Handler 不重复构造同一事实;
  • 从数据库 Restore 聚合时不能重新抛出“已创建”事件。

值对象与 SmartEnum

ValueObject 为无身份的概念提供按值相等语义,例如 EmailAddress。模块可以定义自己的 NoteTitle、金额或范围类型,把校验与规范化集中起来。

SmartEnum<TEnum,TValue> 用于需要稳定名称和值、又需要行为的有限集合。持久化时优先通过显式标量属性和编译期生成的 converter 存储,读取未知值应按该业务契约 fail closed。

W0 列表查询契约新增四类共享筛选值对象:

类型当前语义非法输入
DateRange可单侧无界的时间闭区间;纯日期上界补齐到当日最后一个 tick下界晚于上界返回 DateRange.Invalid
AmountRange使用 decimal 的金额闭区间下限大于上限返回 NumberRange.Invalid
NumberRange<TNumber>数量、评分、百分比等可比较值的闭区间下限大于上限返回 NumberRange.Invalid
EnumFilterSet<TEnum>去重、排序并冻结的枚举集合;相等性忽略输入顺序null 按空集合处理

这些类型的创建入口保护不变量,调用方不能通过公开 setter 构造非法状态。DateRange 的 JSON 转换器也重新调用工厂;数据库过滤使用排他上界,而展示和 Contains 使用闭区间。完整的 HTTP 信封、分页与 QueryShape 展开规则见列表查询与导出范围契约。

平台硬边界

GlobalPlatformConstants 保存不可按租户配置、且被多个模块共同使用的稳定上限。在线列表默认 20、最大 1000,仍由 PagingLimits 定义;GlobalPlatformConstants.Pagination 只提供别名。后台 worker 使用独立的 Batch.PageSize=2000,不能拿它放宽 HTTP 请求。

通用文本边界为:稳定标识 128、短描述 1000、备注与审计原因 2000。远程选项 50、Workflow QueryStore 200、SCIM 200 属于各自协议或引擎,不合并进全局在线分页值。

时钟

IAppClock 是 Domain 与 Application 读取“现在”的统一抽象。默认 SystemClock 从 DI 注入的 TimeProvider 取时;业务代码不得直接读取 DateTime.Now、DateTime.UtcNow、DateTimeOffset.UtcNow 或 TimeProvider.System。聚合行为优先由调用者传入时刻,确需持续读时再注入领域服务。

API类型当前语义适用场景
UtcNowDateTimeOffset注入时间源的 UTC 时刻审计、令牌、过期、领域事件与安全窗口
InstantNodaTime Instant由 UtcNow 转换的绝对时间点跨时区日程、财务日期与需要 NodaTime 的模型
NowDateTimeUTC,Kind=Utc兼容仍使用 DateTime 的旧边界
ToStorage(value)DateTime将绝对时刻归一化为 UTC 存储值旧仓储写边界
FromStorage(value)DateTimeOffset把存储值按 UTC 还原;Unspecified 历史值也按 UTC 解释旧仓储读边界

Clock:Kind 默认为 Utc。Local、Unspecified 与 Clock:TimeZoneId 目前仅保留配置兼容性,ClockNormalize 仍统一按 UTC 读写;不能通过设置 Kind=Local 承诺本地墙钟存储。展示时区转换应留在 API/前端展示边界,不进入审计与持久化事实。

组合根注册 TimeProvider.System;测试可替换为可推进的 TimeProvider。SystemClock 会持续读取该时间源,FixedClock(TimeProvider) 则只在构造时捕获一次。DateTimeExtensions 的 IsPast、IsFuture 与 Elapsed 也有 TimeProvider 重载,确定性测试应使用这些入口。

// 业务用例从显式时钟取得绝对时间,再传给聚合行为。
var occurredAt = clock.UtcNow;
var result = aggregate.Approve(occurredAt, currentUser.UserId);
// 测试通过组合根替换时间源,不修改生产代码。
services.AddSingleton<TimeProvider>(new MutableTimeProvider(fixedUtcNow));

使用边界

  • Domain 可以引用这些原语和模块自身 Contracts。
  • Domain 不引用 Mediator Handler、HttpContext、ORM、CAP、Redis 或配置系统。
  • [BitzTable] / [BitzColumn] 是 Provider 中立的声明元数据,由编译期生成器读取;Domain 不调用 Provider API。
  • 不要把跨模块共享业务规则塞进 Core。共享的是稳定技术语义,模块业务仍由模块所有。

设计审查清单

  • 聚合根是否明确了一致性边界,而不是把一张表机械包装成聚合?
  • 所有预期失败是否返回稳定 Error.Code,并与未预期异常分流?
  • 创建和状态迁移是否通过方法保护不变量,避免公开 setter 绕过规则?
  • 租户数据是否继承正确基类,并从可信上下文取得 TenantId?
  • 领域事件是否描述已经发生的事实,并只携带稳定载荷?
  • Restore 路径是否避免重复抛出创建事件或覆盖审计字段?
  • 时间、当前用户与随机性是否由调用者或显式端口提供?
  • 值对象与 SmartEnum 的未知值、序列化和持久化行为是否有测试?
  • 在线分页、后台批量和协议例外是否使用各自具名边界,而不是复用一个魔法数?

源码与验证入口

核心类型位于 src/Framework/BitzOrcas.Domain;时钟实现位于 src/Framework/BitzOrcas.Infrastructure/Clocking;领域事件收集与管线位于 src/Framework/BitzOrcas.Application。时钟回归由 ApplicationClockTests 覆盖;查询值对象由 PaginationParamsTests、DateRangeTests 和 RangeValueObjectTests 覆盖。新增或修改核心原语后,应同时运行 Domain 单元测试、公开 API/架构测试,以及受影响模块的持久化契约测试。

相关

100%

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