在模块化单体(Modular Monolith)架构中,当出现全新的业务子域(例如“法务知识库与案例裁判文书检索 LegalKnowledge”)时,许多团队因图一时省事,直接将新代码堆砌在既有模块中,或者在单个工程内用文件夹充当“伪模块”。随着业务复杂度上升,不同子域之间的类与数据表互相引用,最终导致单体系统迅速滑向不可维护的“大泥球”(Big Ball of Mud)。
BitzOrcas.Modern 贯彻“物理隔离、契约解耦与编译期治理”的模块设计哲学:
- 三阶物理工程隔离(Three-Tier Projects):每个深模块拆分为
.Contracts、.Domain与宿主实现工程,严禁横向穿透; - 跨模块调用单向契约红线:跨模块同步只允许依赖对方
.Contracts,严禁直接引用内部Domain、Application或Infrastructure; - 跨模块状态变更走事件 Outbox:基于 CAP 与本地事务的 Integration Event 驱动,杜绝跨模块强行开启分布式事务;
IAppModule声明式治理契约:模块必须显式声明自身名称、依赖关系、拥有的 RBAC 权限码、功能特性码以及发布的事件,由框架自动构建依赖图并执行静态门禁;- ArchUnit 架构守卫自动化:通过 CI 自动化单测静态分析程序集依赖拓扑,违规引用将在提交阶段直接阻断构建。
本指南以从零构建**“法务知识库与案例文档管理模块(BitzOrcas.Modules.LegalKnowledge)”**为例,详细演示新增独立模块的全套工程步骤。
模块物理工程布局与依赖方向
第一步:创建 3 个标准物理 .csproj 工程
在 src/Modules/LegalKnowledge/ 目录下建立 3 个具备明确职责划分的物理工程:
1. BitzOrcas.Modules.LegalKnowledge.Contracts.csproj(公开契约层)
对外公开的 DTO、Integration Event、只读 Query 端口。任何外部模块仅能引用此项目:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
<ItemGroup> <ProjectReference Include="..\..\..\Framework\BitzOrcas.Domain\BitzOrcas.Domain.csproj" /> </ItemGroup></Project>2. BitzOrcas.Modules.LegalKnowledge.Domain.csproj(领域内核层)
包含模块的核心聚合根、领域枚举、状态不变量与双 ORM 元数据特性。外部模块绝对禁止引用:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
<ItemGroup> <ProjectReference Include="..\BitzOrcas.Modules.LegalKnowledge.Contracts\BitzOrcas.Modules.LegalKnowledge.Contracts.csproj" /> <ProjectReference Include="..\..\..\Framework\BitzOrcas.Persistence.Metadata\BitzOrcas.Persistence.Metadata.csproj" /> </ItemGroup></Project>3. BitzOrcas.Modules.LegalKnowledge.csproj(应用切片与模块装配根)
承载垂直切片 Handler、Minimal API 端点声明与 IAppModule 装配实现:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
<ItemGroup> <ProjectReference Include="..\BitzOrcas.Modules.LegalKnowledge.Domain\BitzOrcas.Modules.LegalKnowledge.Domain.csproj" /> <ProjectReference Include="..\..\..\Framework\BitzOrcas.Application\BitzOrcas.Application.csproj" /> <ProjectReference Include="..\..\..\Framework\BitzOrcas.Modularity\BitzOrcas.Modularity.csproj" /> </ItemGroup></Project>第二步:编写领域聚合根(Domain Model)
在 BitzOrcas.Modules.LegalKnowledge.Domain 中创建 KnowledgeDocument.cs。
实体继承 TenantAggregateRoot<string>,并通过 [BitzTable] 统一声明 EF Core 与 SqlSugar 双 ORM 映射元数据:
using System;using System.ComponentModel;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.LegalKnowledge.Domain;
/// <summary>/// 法务知识库文档聚合根/// </summary>/// <remarks>/// <para>继承自 <see cref="TenantAggregateRoot{TId}"/>,自动内置主键、租户隔离、审计追踪与并发控制字段。</para>/// <para>持久化映射至 <c>LegalKnowledgeDocument</c> 数据表。</para>/// </remarks>[BitzTable("LegalKnowledgeDocument", IsTenant = true, IsSoftDelete = true, Description = "法务知识库与裁判文书表")][BitzIndex("UX_LegalKnowledge_Tenant_DocCode", "TenantId", "DocumentCode", IsUnique = true)]public sealed class KnowledgeDocument : TenantAggregateRoot<string>{ private const int CodeMaxLength = 64; private const int TitleMaxLength = 200;
/// <summary> /// 知识文档业务唯一编码 /// </summary> [BitzColumn(Length = CodeMaxLength, IsRequired = true, Description = "文档业务唯一编号")] public string DocumentCode { get; private set; } = string.Empty;
/// <summary> /// 知识文档标题 /// </summary> [BitzColumn(Length = TitleMaxLength, IsRequired = true, Description = "文档标题")] public string DocumentTitle { get; private set; } = string.Empty;
/// <summary> /// 知识分类标签 (以分号隔开) /// </summary> [BitzColumn(Length = 256, Description = "法律分类标签")] public string Tags { get; private set; } = string.Empty;
/// <summary> /// 文档正文 Markdown 内容 /// </summary> [BitzColumn(Length = int.MaxValue, Description = "文档正文 Markdown 内容")] public string ContentMarkdown { get; private set; } = string.Empty;
/// <summary> /// 仅供 ORM 底层反序列化物化使用的无参构造 /// </summary> [Obsolete("仅供 ORM 物化使用,业务代码请显式调用 Create 工厂方法。", error: true)] [EditorBrowsable(EditorBrowsableState.Never)] public KnowledgeDocument() : base("0") { }
private KnowledgeDocument( string id, string tenantId, string documentCode, string documentTitle, string tags, string contentMarkdown) : base(id) { TenantId = tenantId; DocumentCode = documentCode; DocumentTitle = documentTitle; Tags = tags; ContentMarkdown = contentMarkdown; }
/// <summary> /// 显式工厂方法:创建法务知识文档聚合根并守护业务不变量 /// </summary> /// <param name="id">主键流水号。</param> /// <param name="tenantId">归属租户标识。</param> /// <param name="documentCode">文档业务编号。</param> /// <param name="documentTitle">文档标题。</param> /// <param name="tags">知识分类标签。</param> /// <param name="contentMarkdown">正文 Markdown。</param> /// <returns>创建成功返回聚合根,失败返回领域错误。</returns> public static Result<KnowledgeDocument> Create( string id, string tenantId, string documentCode, string documentTitle, string tags, string contentMarkdown) { if (string.IsNullOrWhiteSpace(documentTitle)) { return Result.Failure<KnowledgeDocument>(KnowledgeErrors.TitleRequired); }
if (documentTitle.Trim().Length > TitleMaxLength) { return Result.Failure<KnowledgeDocument>(KnowledgeErrors.TitleTooLong); }
if (string.IsNullOrWhiteSpace(documentCode)) { return Result.Failure<KnowledgeDocument>(KnowledgeErrors.CodeRequired); }
var doc = new KnowledgeDocument( id: id, tenantId: tenantId, documentCode: documentCode.Trim().ToUpperInvariant(), documentTitle: documentTitle.Trim(), tags: tags?.Trim() ?? string.Empty, contentMarkdown: contentMarkdown ?? string.Empty);
return Result.Success(doc); }}
/// <summary>/// 法务知识库模块强类型错误字典/// </summary>public static class KnowledgeErrors{ public static readonly Error TitleRequired = Error.Validation("Knowledge.TitleRequired", "知识文档标题不能为空。"); public static readonly Error TitleTooLong = Error.Validation("Knowledge.TitleTooLong", "知识文档标题不能超过 200 个字符。"); public static readonly Error CodeRequired = Error.Validation("Knowledge.CodeRequired", "知识文档业务编号不能为空。");}第三步:实现模块架构治理契约(IAppModule)
在 BitzOrcas.Modules.LegalKnowledge 根目录下实现 IAppModule 接口。
这是 BitzOrcas 模块化单体的“自描述身份证”:框架组合根据此生成依赖拓扑图、初始化模块级 RBAC 动作码,并注册特定策略服务:
using System.Collections.Generic;using BitzOrcas.Modularity;using Microsoft.Extensions.DependencyInjection;
namespace BitzOrcas.Modules.LegalKnowledge;
/// <summary>/// 法务知识库模块架构声明与装配根/// </summary>/// <remarks>/// <para>实现 <see cref="IAppModule"/> 契约,供 Host 组合根在编译期解析拓扑依赖与权限清单。</para>/// <para>红线:跨模块同步调用仅能依赖公开的 Contracts 命名空间。</para>/// </remarks>public sealed class LegalKnowledgeModule : IAppModule{ /// <summary> /// 模块唯一标识符 /// </summary> public string Name => "LegalKnowledge";
/// <summary> /// 模块基础根命名空间 /// </summary> public string BaseNamespace => "BitzOrcas.Modules.LegalKnowledge";
/// <summary> /// 声明当前模块依赖的其他模块名称清单 /// </summary> public IReadOnlyList<string> Dependencies => new[] { "Tenancy" };
/// <summary> /// 当前模块发布的集成事件契约全名 /// </summary> public IReadOnlyList<string> PublishedEvents => new[] { "BitzOrcas.Modules.LegalKnowledge.Contracts.Events.KnowledgeDocumentPublishedIntegrationEvent" };
/// <summary> /// 当前模块订阅的外部集成事件契约全名 /// </summary> public IReadOnlyList<string> SubscribedEvents => new[] { "BitzOrcas.Modules.Legal.Contracts.Events.MatterIntakeCreatedIntegrationEvent" };
/// <summary> /// 对外公开暴露的契约命名空间清单 (其他模块只允许引用这些命名空间) /// </summary> public IReadOnlyList<string> PublicContractNamespaces => new[] { "BitzOrcas.Modules.LegalKnowledge.Contracts", "BitzOrcas.Modules.LegalKnowledge.Contracts.Dtos", "BitzOrcas.Modules.LegalKnowledge.Contracts.Events" };
/// <summary> /// 当前模块声明拥有的细粒度 RBAC 权限码 /// </summary> public IReadOnlyList<string> OwnedPermissions => new[] { "legal.knowledge.view", "legal.knowledge.create", "legal.knowledge.publish", "legal.knowledge.delete" };
/// <summary> /// 当前模块声明拥有的功能特性开关码 /// </summary> public IReadOnlyList<string> OwnedFeatures => new[] { "legal.knowledge.fulltext-search" };
/// <summary> /// 注册模块特有的非机械策略服务 (如专用搜索引擎适配器、特定本地缓存策略) /// </summary> /// <param name="services">DI 服务容器。</param> /// <remarks> /// 常规的仓储、垂直切片 Handler 由 Roslyn 增量生成器编译期自动注册,此方法仅保留特殊策略注册。 /// </remarks> public void ConfigureServices(IServiceCollection services) { // 可在此按需注册特定服务,如向量检索适配器或本地只读缓存 }}第四步:编写 ArchUnit 架构守卫测试,物理锁死边界
在 tests/BitzOrcas.Architecture.Tests/ModuleBoundaryTests.cs 中添加自动化防线:
确保没有任何外部业务模块(如 Legal 或 Billing)越权依赖 LegalKnowledge 内部的 Domain 或应用实现:
using ArchUnitNET.Domain;using ArchUnitNET.Fluent;using ArchUnitNET.Loader;using ArchUnitNET.xUnit;using Xunit;using static ArchUnitNET.Fluent.ArchRuleDefinition;
namespace BitzOrcas.Architecture.Tests;
/// <summary>/// 模块化单体物理边界架构守卫自动化测试/// </summary>public sealed class ModuleBoundaryTests{ private static readonly Architecture SolutionArchitecture = new ArchLoader().LoadAssemblies( typeof(LegalKnowledge.LegalKnowledgeModule).Assembly, typeof(LegalKnowledge.Domain.KnowledgeDocument).Assembly, typeof(LegalKnowledge.Contracts.Dtos.KnowledgeDocumentSummaryDto).Assembly).Build();
[Fact] public void ExternalModules_MustNotDependOn_LegalKnowledgeInternalDomainOrImplementation() { // 1. 定义模块边界防线规则:禁止除自己以外的任何命名空间依赖 LegalKnowledge 的 Domain IArchRule rule = Types().That() .ResideInNamespace("BitzOrcas.Modules..") .And() .DoNotResideInNamespace("BitzOrcas.Modules.LegalKnowledge..") .ShouldNot() .DependOnAny(Types().That().ResideInNamespace("BitzOrcas.Modules.LegalKnowledge.Domain.."));
// 2. 执行静态程序集分析与依赖拓扑检查 rule.Check(SolutionArchitecture); }}总结
遵循三阶物理工程规范新增独立模块,为系统提供了长期的工程确定性:
- 物理工程隔离:通过独立的
.Contracts、.Domain和宿主工程,从编译器层面阻止跨模块非法直接引用; - 自描述治理元数据:
IAppModule使得权限注册、事件总线路由与功能特性开关完全自动化; - 架构防线常驻:ArchUnit 自动化测试作为 CI 门禁,将架构设计原则转化为可执行的机器检验,永不退化。