Skip to content
bitzorcas
中EN

Tutorial

从零创建业务模块:像搭积木一样交付高内聚领域

遵循 BitzOrcas.Modern 模块化单体标准,从零构建法务合同管理模块(LegalContract):划分 Contracts/Domain/Application 物理工程边界、实现 IAppModule 治理元数据、继承 TenantAggregateRoot 统一聚合根,并通过 ArchUnitNET 架构测试建立不可逾越的依赖守护红线。

Last updated

单体架构演进中最致命的陷阱不是业务逻辑复杂,而是缺乏物理约束的依赖腐败(Unchecked Dependency Erosion)。在很多团队中,新员工为了“快速实现需求”,直接在订单模块中注入用户模块的内部实体、甚至书写跨模块表的隐式 SQL JOIN。不出一年,系统就会退化为牵一发而动全身的大泥球(Big Ball of Mud),团队最终不得不花费数倍代价进行痛苦的重构。

BitzOrcas.Modern 将“可执行的架构约束”作为第一信条。通过程序集层面的物理边界隔离、IAppModule 自持治理清单,以及 CI 门禁中的 ArchUnitNET 架构单元测试,让每个业务模块像微服务一样边界清晰、自治可替换,同时尽享单体进程内的零网络延迟与流畅调试体验。

本教程将以真实的**法务合同管理模块(LegalContract)**为例,带你走通从工程分层、契约定义到架构门禁校验的全生命周期。

模块物理工程拓扑与依赖铁律

在 BitzOrcas.Modern 中,任何业务模块均被划分到严格单向依赖的三层物理项目中:

外部调用方模块 (例如: Billing 计费中心)接入与宿主组合根 (API Host / AppHost)只允许引用公开契约包严禁跨模块穿透引用实现严禁跨模块穿透引用实现BitzOrcas.Modules.LegalContract 业务自治模块

1. LegalContract.Contracts
(公开接口、只读 DTO 与集成事件契约)

3. LegalContract.Application
(垂直切片用例、IRequestRule 校验、Handler 与 IAppModule)

2. LegalContract.Domain
(合同统一聚合根、枚举、值对象与领域不变量)

BitzOrcas.Api (路由聚合与依赖装配)

SaaS 计费结算 Handler


第一步:划分模块物理工程与目录结构

在 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 中定义对外广播的集成事件与只读查询模型:

src/Modules/Business/LegalContract/BitzOrcas.Modules.LegalContract.Contracts/Events/ContractSignedIntegrationEvent.cs
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>:

src/Modules/Business/LegalContract/BitzOrcas.Modules.LegalContract.Domain/Aggregates/Contract.cs
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 契约,向治理中心声明自身边界:

src/Modules/Business/LegalContract/BitzOrcas.Modules.LegalContract.Application/LegalContractModule.cs
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/ 目录下添加模块架构门禁:

tests/BitzOrcas.Architecture.Tests/LegalContractModuleBoundaryTests.cs
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% 绿灯全通过?

100%

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