架构测试把“代码应该怎样组织”变成提交时可执行的合同。BitzOrcas.Architecture.Tests 不只扫描程序集依赖:它还解析 .csproj、源码、JSON、YAML 和架构文档,阻止旧 mapper、运行时反射、错误的跨模块引用、失真的商业包清单和已删除浅模块重新进入主干。
它在证据链中的位置
它证明结构仍符合已接受的设计,却不能证明 SQL、事务或消息在真实基础设施上正确;后者属于集成合同。反过来,一条 API 能跑通,也不能证明依赖方向没有退化。
当前守护面
| 守护面 | 典型事实 | 主要证据 |
|---|---|---|
| 分层依赖 | Domain 不依赖 Application/Infrastructure/Api | 编译程序集、.csproj |
| 模块协作 | 跨模块只经过允许的 Contracts seam | 项目引用图、例外登记 |
| 持久化 | Application 无 ORM 包;聚合与 metadata 规则成立 | 项目 XML、源码、生成清单 |
| AOT | 禁止未登记的运行时反射路径 | 源码扫描、trim 门禁契约 |
| 生成器 | DI、Endpoint、Query、Persistence metadata 走编译期输出 | 生成器源码和消费测试 |
| 商业交付 | catalog、Profile closure、packable 项目一致 | JSON、项目属性、目录扫描 |
| Consumer | 模板不复制核心源码、不回引产品仓库 | 临时消费项目与源码契约 |
| CI | Job、触发、timeout、Docker 分片、GA fail-closed | Workflow YAML 文本合同 |
| 删除门禁 | 浅模块、旧 mapper、旧 query reflection 不得复活 | 全局路径与 token sweep |
仓库当前有大量按风险命名的规则类,例如 CsprojDependencyGraphTests、ReflectionAotGuardTests、CommercialPackageCatalogTests、DockerContractTraitTests 与 ShallowModuleDeletionGateTests。寻找现有归属比新建“万能 ArchitectureTests”更容易维护。
三种规则实现
编译后依赖规则
程序集规则回答“最终制品实际引用了谁”。Workflow 的规则会加载 BitzOrcas.Workflow.Engine,确认它不引用 SqlSugar、EF Core、Dapper、ASP.NET Core 或任何 Workflow adapter。这种检查比命名约定更接近最终二进制事实。
[Theory][InlineData("BitzOrcas.Workflow.Engine")]public void WorkflowEngine_ShouldNotReferenceAdapters(string assemblyName){ // 读取编译后直接依赖,避免只看源码 using 得出错误结论。 var references = Assembly.Load(assemblyName) .GetReferencedAssemblies() .Select(item => item.Name) .ToArray();
// 逐项写出禁止引用,使失败能直接指出被破坏的边界。 references.ShouldNotContain("SqlSugar"); references.ShouldNotContain("Microsoft.EntityFrameworkCore"); references.ShouldNotContain("BitzOrcas.Workflow.EfCore");}项目图与机器可读合同
CsprojDependencyGraphTests 解析所有适用的 .csproj,区分 ProjectReference 与 PackageReference,并读取显式例外登记。它能发现 Application 直接引用 ORM 包、Contracts 反向引用实现层、模块 A 直接引用模块 B 的 Application 等问题。
var project = XDocument.Load(projectPath);
// ProjectReference 决定内部层级方向;PackageReference 负责识别 ORM 泄漏。var projectReferences = project.Descendants("ProjectReference") .Select(node => node.Attribute("Include")?.Value) .Where(value => value is not null) .ToArray();
// 此处只表达 Application→Infrastructure 红线;其他层级由各自规则负责。projectReferences.ShouldNotContain(path => path!.Contains(".Infrastructure", StringComparison.Ordinal));源码与删除门禁
有些合同无法从程序集还原,例如“指定命令必须使用状态流转模板”“旧目录必须不存在”“CI 必须保留 13 个 Docker 分片”。这时规则读取源码或 Workflow 文件,并给出完整违规路径。源码 token 适合守住明确、稳定的结构事实,不适合推断复杂语义。
运行与缩小范围
# 合并前运行完整架构规则集。dotnet test tests/BitzOrcas.Architecture.Tests --configuration Release
# 只验证 CI 质量门禁合同;类名 filter 比方法片段更稳定。dotnet test tests/BitzOrcas.Architecture.Tests \ --configuration Release \ --filter 'FullyQualifiedName~CiQualityGateTests'规则依赖 Release 编译制品时,不要拿旧的 bin/Debug 结果解释失败。使用 --no-build 前必须确认同一 Commit、同一 Configuration 已完成构建。
新增规则的完整步骤
- 写出被防止的具体回归,例如“Platform Application 不得 PackageReference ORM”,不要写“架构要干净”。
- 找到权威 ADR、约束或机器可读清单;如果决策尚未存在,先补决策。
- 选择最接近事实的证据:程序集、项目 XML、manifest、源码路径或 CI YAML。
- 让测试在违规夹具或临时改动上失败,确认不是永远为绿的断言。
- 输出全部 offender、规则原因和修复方向,不只返回
false。 - 若确有例外,登记 owner、理由和退出条件;不要把宽泛目录加入 allowlist。
- 运行定向规则、完整 Architecture.Tests,再运行所属业务测试。
失败定位
先看失败类名,它通常已经表达归属。依赖图失败时检查 offending .csproj 和例外清单;源码门禁失败时检查路径是否移动、语义是否变化;manifest 失败时同时比较清单生产者与消费者。只有架构决策真的改变时,才同步修改规范、测试和实现。
评审清单
- 规则名表达业务/架构事实,而不是实现动作;
- 扫描范围覆盖新增模块,但排除项明确;
- 失败消息列出所有 offender 与权威规则;
- Windows/Linux 路径分隔和大小写差异已考虑;
- 例外是精确条目,不是目录级永久豁免;
- 结构测试没有冒充运行时、性能或安全证明;
- 修改后执行全局
rg/findsweep,预期旧模式结果为零。