Skip to content
bitzorcas
中EN

Recipe

实战:从零新增一个独立业务模块(Module)

掌握在 BitzOrcas.Modern 中新增独立深模块的完整工程规范:规划 Contracts/Domain/Application 物理工程划分、继承 TenantAggregateRoot 领域聚合根、配置 IAppModule 架构治理元数据与编写 ArchUnit 架构守卫防线。

Last updated

在模块化单体(Modular Monolith)架构中,当出现全新的业务子域(例如“法务知识库与案例裁判文书检索 LegalKnowledge”)时,许多团队因图一时省事,直接将新代码堆砌在既有模块中,或者在单个工程内用文件夹充当“伪模块”。随着业务复杂度上升,不同子域之间的类与数据表互相引用,最终导致单体系统迅速滑向不可维护的“大泥球”(Big Ball of Mud)。

BitzOrcas.Modern 贯彻“物理隔离、契约解耦与编译期治理”的模块设计哲学:

  1. 三阶物理工程隔离(Three-Tier Projects):每个深模块拆分为 .Contracts、.Domain 与宿主实现工程,严禁横向穿透;
  2. 跨模块调用单向契约红线:跨模块同步只允许依赖对方 .Contracts,严禁直接引用内部 Domain、Application 或 Infrastructure;
  3. 跨模块状态变更走事件 Outbox:基于 CAP 与本地事务的 Integration Event 驱动,杜绝跨模块强行开启分布式事务;
  4. IAppModule 声明式治理契约:模块必须显式声明自身名称、依赖关系、拥有的 RBAC 权限码、功能特性码以及发布的事件,由框架自动构建依赖图并执行静态门禁;
  5. ArchUnit 架构守卫自动化:通过 CI 自动化单测静态分析程序集依赖拓扑,违规引用将在提交阶段直接阻断构建。

本指南以从零构建**“法务知识库与案例文档管理模块(BitzOrcas.Modules.LegalKnowledge)”**为例,详细演示新增独立模块的全套工程步骤。

模块物理工程布局与依赖方向

其他业务模块 (如 Legal / Billing)BitzOrcas.Modules.LegalKnowledge 业务模块API Host 组合根 (src/Hosts/BitzOrcas.Api)只允许依赖公开契约

Program.cs (静态调用 AddBitzModules)

1. BitzOrcas.Modules.LegalKnowledge.Contracts
(公开 DTO、查询端口与集成事件)

2. BitzOrcas.Modules.LegalKnowledge.Domain
(知识库聚合根、枚举与不变量)

3. BitzOrcas.Modules.LegalKnowledge
(垂直切片、Handler 与 IAppModule 装配根)

BitzOrcas.Modules.Legal


第一步:创建 3 个标准物理 .csproj 工程

在 src/Modules/LegalKnowledge/ 目录下建立 3 个具备明确职责划分的物理工程:

1. BitzOrcas.Modules.LegalKnowledge.Contracts.csproj(公开契约层)

对外公开的 DTO、Integration Event、只读 Query 端口。任何外部模块仅能引用此项目:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.Contracts/BitzOrcas.Modules.LegalKnowledge.Contracts.csproj
<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 元数据特性。外部模块绝对禁止引用:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.Domain/BitzOrcas.Modules.LegalKnowledge.Domain.csproj
<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 装配实现:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.csproj
<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 映射元数据:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.Domain/KnowledgeDocument.cs
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 动作码,并注册特定策略服务:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge/LegalKnowledgeModule.cs
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 或应用实现:

tests/BitzOrcas.Architecture.Tests/ModuleBoundaryTests.cs
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 门禁,将架构设计原则转化为可执行的机器检验,永不退化。

100%

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