Skip to content
bitzorcas
中EN

Reference

共享基础构建块

Result、错误、时间、调用者、租户、种子框架与共享抽象归属规则的源码核验参考。

Last updated

Shared 的价值不在于“少写几遍代码”,而在于给所有模块提供少量、稳定、没有业务所有者的基础语义。错误地把业务 DTO、实体、仓储接口或通用 Helper 放进 Shared,会把显式模块依赖变成全局隐性耦合。

归属判断

是否是否否是

准备共享一个类型

是否含业务术语或规则?

放回 owner 模块 Contracts

是否隐藏 IO、时间、租户或全局状态?

改成显式端口,由 Host 装配

语义是否稳定且被多个模块长期使用?

保留在当前模块

进入 Framework 基础构建块

“目前有两个调用方”不是共享理由。类型若可能由某个业务模块独立演进,就应该由该模块拥有;调用方通过 Contracts 依赖它。真正的基础抽象还必须保持轻依赖,不能反向引用 Platform 或 Modules。

构建块真实职责典型误用
Result / Error表达预期业务失败与稳定错误分类用异常表示校验失败
IAppClock统一当前时刻和持久化时间转换在 Handler 中调用 DateTime.Now
ICurrentUser提供认证后可信身份快照相信请求体中的 TenantId
ITenantContext当前租户访问器的只读兼容投影保存第二份可写租户状态
Seed framework有序、可审计地执行 ORM 中立种子步骤启动时无条件插入演示账号

Result 与 Error

Domain 和 Application 的预期失败使用 Result。Error.Code 是稳定的定位与 i18n 键,建议遵循 {Module}.{Scenario}.{Reason};Description 只用于开发诊断,不能直接作为终端用户文案。ErrorType 决定 Web 边界的 HTTP 分类。

public static class BillingErrors
{
public static readonly Error AmountInvalid =
Error.Validation("Billing.Refund.AmountInvalid", "退款金额必须大于零。");
public static readonly Error ExceedsPaidAmount =
Error.Failure("Billing.Refund.ExceedsPaidAmount", "退款金额不能超过已支付金额。");
}
public static Result<Money> CreateRefund(decimal amount, decimal paidAmount)
{
if (amount <= 0)
{
// 稳定错误码供 API、客户端和审计关联;描述不是用户翻译文本。
return Result.Failure<Money>(BillingErrors.AmountInvalid);
}
if (amount > paidAmount)
{
// 可预期的规则不满足用 Failure,不抛业务异常。
return Result.Failure<Money>(BillingErrors.ExceedsPaidAmount);
}
return Result.Success(new Money(amount, "CNY"));
}

Validation、NotFound、Conflict、Forbidden、Unauthorized、Failure 和 Unexpected 是不同语义,不要为了得到某个状态码而错选类型。异常仍适用于程序错误、损坏的不变量和无法恢复的基础设施故障,并由全局异常处理器统一收口。

时间、调用者与租户

需要“现在”的领域或应用逻辑注入 IAppClock。UtcNow 用于绝对时刻,Instant 用于 NodaTime 场景,Now 遵循统一 ClockOptions.Kind;仓储边界通过 ToStorage / FromStorage 对称转换。业务代码不自行推断数据库时区。

ICurrentUser.User 是认证基础设施构造的 CurrentUser。授权和数据范围使用 EffectiveUserId,审计真实操作者使用 ActorUserId;委托场景下两者可能不同。租户标识来自可信上下文,不由 DTO 覆盖。

public static class ApprovalErrors
{
public static readonly Error CallerRequired =
Error.Unauthorized("Approval.Caller.Required", "需要已认证调用者。");
}
public sealed class ApproveHandler(ICurrentUser currentUser, IAppClock clock)
{
public Result<Approval> Handle(ApproveCommand command)
{
var caller = currentUser.User;
if (!caller.IsAuthenticated || caller.EffectiveUserId is null)
{
// 身份缺失是类型化失败;不要直接读取 HttpContext 或请求头。
return Result.Failure<Approval>(ApprovalErrors.CallerRequired);
}
// 有效用户用于业务动作,真实操作者另行写入审计字段。
return Approval.Create(
command.SubjectId,
caller.EffectiveUserId.Value,
caller.ActorUserId,
clock.UtcNow);
}
}

ITenantContext 只是 ICurrentTenantAccessor.Current 的只读兼容门面,只有 TenantId 和可选 OfficeId。新的可写作用域应通过 ICurrentTenantAccessor.BeginScope 建立,避免异步流中残留或出现两份互相矛盾的当前租户。

种子框架

ISeedRunner 按 Order、再按 SeedId 排序串行执行。重复 SeedId、重复 Order 和不存在的依赖会 fail-loud。Demo 步骤在 Production 与 Staging 自动跳过;取消会继续抛出,普通异常写入 SeedRunReport,是否停止由 StopOnError 决定。

public sealed class CountrySeedStep(ICountrySeedStore store) : ISeedStep
{
public int Order => 120;
public string SeedId => "sys_country";
public int Version => 2;
public string[] DependsOn => [];
public async Task ExecuteAsync(string environment, CancellationToken ct)
{
// 用稳定业务代码 Upsert;不能依赖数据库自增主键判断是否已执行。
await store.UpsertAsync("CN", "中国", ct);
await store.UpsertAsync("SG", "新加坡", ct);
// 重复运行必须收敛到相同状态,不能产生重复记录。
}
}

BitzOrcas:Seed 可配置 Enabled、AutoRunOnStartup、StopOnError、OverrideRootPath、CsvDelimiter 与 SkipSeedIds。AutoRunOnStartup 默认关闭。生产种子不得包含固定密码、演示账号或不可轮换 Token;CSV 磁盘覆盖是运维能力,也意味着发布流程必须校验来源与完整性。

值对象、脱敏与审计端口

Email、金额、标识等值对象的格式和不变量属于 Domain,并以 Result 创建。跨模块脱敏使用统一 Masker;不要在每个 Handler 复制正则。审计能力以 Application 端口表达,由 Host 选择生产适配器,使领域模型既不依赖 HTTP,也不依赖日志或数据库实现。

Shared 不是“所有横切逻辑”的收纳箱。缓存、锁、事件和审计即使被多模块使用,也有独立行为、配置和失败策略,应维持各自构建块边界。

测试与演进门禁

基础构建块的改动影响面大,应以公开 API 测试、依赖方向测试和跨模块契约测试保护。新增类型进入 Shared 前至少回答:谁负责版本兼容、是否引入业务语言、是否隐藏外部状态、是否能由更具体的 owner 契约替代。

测试还应覆盖 Result 成功/失败不变量、委托身份的 Actor/Effective 区分、时钟转换对称性、种子重复与依赖诊断,以及 Demo 环境门禁。任何破坏性变更都应先迁移调用方,再删除旧抽象。

进一步阅读:核心构建块、数据脱敏、持久化。

源码定位

  • src/Framework/BitzOrcas.Domain/Results/
  • src/Framework/BitzOrcas.Domain/Abstractions/IAppClock.cs
  • src/Framework/BitzOrcas.Application/Abstractions/Users/
  • src/Framework/BitzOrcas.Infrastructure/Seeders/

100%

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