单体架构演进中最致命的陷阱不是业务逻辑复杂,而是缺乏物理约束的依赖腐败(Unchecked Dependency Erosion)。在很多团队中,新员工为了“快速实现需求”,直接在订单模块中注入用户模块的内部实体、甚至书写跨模块表的隐式 SQL JOIN。不出一年,系统就会退化为牵一发而动全身的大泥球(Big Ball of Mud),团队最终不得不花费数倍代价进行痛苦的重构。
BitzOrcas.Modern 将“可执行的架构约束”作为第一信条。通过程序集层面的物理边界隔离、IAppModule 自持治理清单,以及 CI 门禁中的 ArchUnitNET 架构单元测试,让每个业务模块像微服务一样边界清晰、自治可替换,同时尽享单体进程内的零网络延迟与流畅调试体验。
本教程将以真实的**法务合同管理模块(LegalContract)**为例,带你走通从工程分层、契约定义到架构门禁校验的全生命周期。
模块物理工程拓扑与依赖铁律
在 BitzOrcas.Modern 中,任何业务模块均被划分到严格单向依赖的三层物理项目中:
第一步:划分模块物理工程与目录结构
在 src/Modules/Business/ 下为新业务域创建独立的物理目录结构:
src/Modules/Business/LegalContract/├── BitzOrcas.Modules.LegalContract.Contracts/ # 公开契约层 (被外部模块引用,零内部实现)│ ├── Events/ # 跨模块集成事件契约│ ├── Queries/ # 跨模块只读查询端口契约│ └── Dtos/ # 数据传输对象 (DTOs)├── BitzOrcas.Modules.LegalContract.Domain/ # 领域内核层 (纯业务状态与规则)│ ├── Aggregates/ # 继承 TenantAggregateRoot 的统一聚合根│ └── ValueObjects/ # 强类型值对象与领域枚举└── BitzOrcas.Modules.LegalContract.Application/ # 应用实现层 (垂直切片命令与调度) ├── Commands/ # 垂直切片写模型 (One File Use Case) ├── Queries/ # 垂直切片读模型 (高吞吐投影) └── LegalContractModule.cs # 模块治理清单 (实现 IAppModule)第二步:定义跨模块公开契约(Contracts)
在 BitzOrcas.Modules.LegalContract.Contracts 中定义对外广播的集成事件与只读查询模型:
using BitzOrcas.Domain.Contracts;
namespace BitzOrcas.Modules.LegalContract.Contracts.Events;
/// <summary>/// 合同已签署跨模块集成事件/// </summary>/// <remarks>/// 由法务合同模块在本地数据库事务提交时原子落入 CAP 发件箱表,/// 后台守护进程可靠投递至 RabbitMQ,供 Billing 计费模块异步扣减企业账户额度。/// </remarks>public sealed record ContractSignedIntegrationEvent( string ContractId, string ContractCode, string TenantId, string ClientName, decimal TotalAmount, DateTimeOffset SignedAtUtc) : IDomainEvent;第三步:实现领域统一聚合根(Domain)
在 BitzOrcas.Modules.LegalContract.Domain 中创建合同聚合根,继承框架提供的 TenantAggregateRoot<string>:
using System;using System.ComponentModel;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.LegalContract.Domain.Aggregates;
/// <summary>/// 合同领域稳定错误契约/// </summary>public static class ContractErrors{ /// <summary> /// 合同金额非法 /// </summary> public static readonly Error InvalidAmount = Error.Validation("Contract.InvalidAmount", "合同金额不能为负数。");
/// <summary> /// 合同当前状态不允许该操作 /// </summary> public static readonly Error InvalidState = Error.Conflict("Contract.InvalidState", "仅草稿状态的合同允许执行签署操作。");}
/// <summary>/// 法务合同统一聚合根/// </summary>[BitzTable("LegalContract", IsTenant = true, IsSoftDelete = true, Description = "企业法务合同台账")]public sealed class Contract : TenantAggregateRoot<string>{ private const int CodeMaxLength = 32; private const int TitleMaxLength = 200;
[BitzColumn(Length = CodeMaxLength, IsRequired = true, IsUnique = true, Description = "合同全局编号")] public string ContractCode { get; private set; } = string.Empty;
[BitzColumn(Length = TitleMaxLength, IsRequired = true, Description = "合同标题")] public string Title { get; private set; } = string.Empty;
[BitzColumn(Precision = 18, Scale = 2, IsRequired = true, Description = "合同总金额")] public decimal TotalAmount { get; private set; }
[BitzColumn(IsRequired = true, Description = "合同当前签署状态")] public ContractStatus Status { get; private set; } = ContractStatus.Draft;
/// <summary> /// 仅供 ORM 持久化物化使用的空构造函数 /// </summary> [Obsolete("仅供 ORM 持久化物化使用。请使用 Draft 工厂方法。", error: true)] [EditorBrowsable(EditorBrowsableState.Never)] public Contract() : base("0") { }
private Contract( string id, string contractCode, string title, decimal totalAmount, string tenantId) : base(id) { ContractCode = contractCode; Title = title; TotalAmount = totalAmount; TenantId = tenantId; Status = ContractStatus.Draft; }
/// <summary> /// 工厂方法:草拟新合同 /// </summary> public static Result<Contract> Draft( string id, string contractCode, string title, decimal totalAmount, string tenantId) { if (totalAmount < 0) { return Result<Contract>.Failure(ContractErrors.InvalidAmount); }
var contract = new Contract(id, contractCode.Trim(), title.Trim(), totalAmount, tenantId); return Result<Contract>.Success(contract); }
/// <summary> /// 业务方法:执行合同签署并标记状态 /// </summary> public Result Sign() { if (Status != ContractStatus.Draft) { return Result.Failure(ContractErrors.InvalidState); }
Status = ContractStatus.Signed; return Result.Success(); }}
public enum ContractStatus{ Draft = 1, Signed = 2, Terminated = 3}第四步:自持模块治理描述符(IAppModule)
在应用层根目录创建 LegalContractModule.cs,实现框架的 IAppModule 契约,向治理中心声明自身边界:
using BitzOrcas.Modularity;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;
namespace BitzOrcas.Modules.LegalContract.Application;
/// <summary>/// 法务合同模块治理声明与组合根/// </summary>public sealed class LegalContractModule : IAppModule{ public string Name => "LegalContract";
public string BaseNamespace => "BitzOrcas.Modules.LegalContract";
/// <summary> /// 声明模块依赖(例如本模块依赖 Identity 模块提供的租户与用户上下文) /// </summary> public IReadOnlyList<string> Dependencies => ["Identity", "Authorization"];
/// <summary> /// 声明对外发布的集成事件 /// </summary> public IReadOnlyList<string> PublishedEvents => [ "BitzOrcas.Modules.LegalContract.Contracts.Events.ContractSignedIntegrationEvent" ];
/// <summary> /// 声明订阅的外部集成事件 /// </summary> public IReadOnlyList<string> SubscribedEvents => [];
/// <summary> /// 声明公开的契约命名空间 /// </summary> public IReadOnlyList<string> PublicContractNamespaces => [ "BitzOrcas.Modules.LegalContract.Contracts" ];
/// <summary> /// 声明本模块拥有的权限码 /// </summary> public IReadOnlyList<string> OwnedPermissions => [ "LegalContract.Contract.Create", "LegalContract.Contract.View", "LegalContract.Contract.Sign" ];
/// <summary> /// 注册特定业务策略(机械仓储与 Mediator 由 Roslyn 增量生成器全自动接管,此处无需手工注册) /// </summary> public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { // 仅注册模块特化的外部适配器或策略钩子 }}第五步:ArchUnitNET 架构测试守门(可执行架构约束)
口头规范无法阻止架构退化,必须依赖机器级自动化测试强制断言。
在 tests/BitzOrcas.Architecture.Tests/ 目录下添加模块架构门禁:
using System.Reflection;using ArchUnitNET.Fluent;using ArchUnitNET.Loader;using Shouldly;using Xunit;using static ArchUnitNET.Fluent.ArchRuleDefinition;
public class LegalContractModuleBoundaryTests{ private static readonly Assembly ContractsAsm = Assembly.Load("BitzOrcas.Modules.LegalContract.Contracts"); private static readonly Assembly DomainAsm = Assembly.Load("BitzOrcas.Modules.LegalContract.Domain"); private static readonly Assembly ApplicationAsm = Assembly.Load("BitzOrcas.Modules.LegalContract.Application");
private static readonly ArchUnitNET.Domain.Architecture Architecture = new ArchLoader() .LoadAssemblies(ContractsAsm, DomainAsm, ApplicationAsm) .Build();
[Fact] public void Contracts_Should_Not_Reference_Domain_Or_Application() { // 门禁 1:公开契约层绝不允许反向引用业务实现层或领域内核 var references = ContractsAsm.GetReferencedAssemblies().Select(a => a.Name).ToArray();
references.ShouldNotContain("BitzOrcas.Modules.LegalContract.Domain"); references.ShouldNotContain("BitzOrcas.Modules.LegalContract.Application"); }
[Fact] public void External_Modules_Should_Only_Reference_Contracts() { // 门禁 2:验证外部模块(如 Billing)仅允许依赖 Contracts,严禁穿透引用实现 var rule = Types().That().ResideInNamespace("BitzOrcas.Modules.Billing..") .Should().NotDependOnAny( Types().That().ResideInNamespace("BitzOrcas.Modules.LegalContract.Domain..") .Or().ResideInNamespace("BitzOrcas.Modules.LegalContract.Application.."));
rule.Evaluate(Architecture).HasViolations.ShouldBeFalse(); }}模块交付核对清单(Checklist)
完成新模块开发并准备提交 PR 时,请逐一核对以下 5 条铁律:
- 物理工程是否严格拆分为
Contracts、Domain与Application? - 聚合根是否继承自
TenantAggregateRoot<TId>并标注[BitzTable]元数据? - 是否在
LegalContractModule.cs中完整声明了权限码与发布事件清单? - 跨模块数据同步是否仅通过 CAP 集成事件或
Contracts中的只读 Port 进行? - 终端运行
dotnet test tests/BitzOrcas.Architecture.Tests是否 100% 绿灯全通过?