"事件风暴 → 上下文映射 → 模块化"在 ABP vNext 的全链路模板 ✨
📚 目录
- ["事件风暴 → 上下文映射 → 模块化"在 ABP vNext 的全链路模板 ✨](#“事件风暴 → 上下文映射 → 模块化”在 ABP vNext 的全链路模板 ✨)
-
- 0) 摘要(TL;DR)📝 摘要(TL;DR)📝)
- 1) 工作坊与产出物 🤝 工作坊与产出物 🤝)
-
- [1.1 事件风暴(Event Storming)](#1.1 事件风暴(Event Storming))
- [1.2 从事件风暴到上下文映射(Context Mapping)](#1.2 从事件风暴到上下文映射(Context Mapping))
- 2) 映射到 ABP 模块边界(工程化落地)🏗️ 映射到 ABP 模块边界(工程化落地)🏗️)
-
- [2.1 模块命名与分层(建议 7 层)](#2.1 模块命名与分层(建议 7 层))
- [2.2 ABP CLI:创建解决方案与模块](#2.2 ABP CLI:创建解决方案与模块)
- 3) 上下文通信:HttpApi.Client 动态代理 + Polly(重试/熔断)🔗 上下文通信:HttpApi.Client 动态代理 + Polly(重试/熔断)🔗)
- 4) 一致性:分布式事件总线 + Outbox/Inbox(EF Core)📨 一致性:分布式事件总线 + Outbox/Inbox(EF Core)📨)
- 5) 多租户与数据边界 🏢 多租户与数据边界 🏢)
- 6) 契约门禁(流水线守门员)🛡️ 契约门禁(流水线守门员)🛡️)
-
- [6.1 API 契约:Swashbuckle CLI + oasdiff(检测破坏性变更)](#6.1 API 契约:Swashbuckle CLI + oasdiff(检测破坏性变更))
- [6.2 架构门禁:NetArchTest / ArchUnitNET](#6.2 架构门禁:NetArchTest / ArchUnitNET)
- 7) 伴随测试(从用例到回归)🧪 伴随测试(从用例到回归)🧪)
-
- [7.1 契约测试(PactNet v4 Consumer 示例)------**已换为 `.WithHttpInteractions()`**](#7.1 契约测试(PactNet v4 Consumer 示例)——已换为
.WithHttpInteractions()
) - [7.2 组件/集成:Testcontainers(PostgreSQL/RabbitMQ)](#7.2 组件/集成:Testcontainers(PostgreSQL/RabbitMQ))
- [7.1 契约测试(PactNet v4 Consumer 示例)------**已换为 `.WithHttpInteractions()`**](#7.1 契约测试(PactNet v4 Consumer 示例)——已换为
- 8) 分阶段迁移老系统(Strangler Fig)🌿 分阶段迁移老系统(Strangler Fig)🌿)
- 9) 可观测与演进度量 📈 可观测与演进度量 📈)
- 10) 工程骨架(落地目录)🗂️ 工程骨架(落地目录)🗂️)
0) 摘要(TL;DR)📝
本文交付一套从业务共创工作坊 到可运行工程骨架 的闭环:
事件风暴 → 子域/上下文划分 → 上下文映射(关系/协作模式) → ABP 模块边界与依赖矩阵 → 契约门禁(CI) → 伴随测试 → 分阶段迁移老系统 → 持续度量与反模式清单。
总览图(从白板到上线):
🟧 事件风暴
Event Storming 🟦 上下文映射
Context Mapping 🟪 ABP 模块边界
七层分离 & DependsOn 🟩 契约清单
HTTP OpenAPI / 消息 Schema 🛡️ CI 门禁
oasdiff / ArchRules / Pact 🧪 伴随测试
单元/集成/Testcontainers 🌱 渐进迁移
Strangler Fig 📈 可观测 & 度量
耦合/破坏率/SLO
1) 工作坊与产出物 🤝
1.1 事件风暴(Event Storming)
- 自上而下:Big Picture → Process/Design level。
- 把"命令 → 领域事件 → 聚合 → 读模型"排成时间线,沉淀统一语言(UL),形成"能力清单"。
仓库产出模板:
/docs/event-storming/board.md # 事件清单/照片转录
/docs/event-storming/glossary.yaml # 统一语言词典
/docs/event-storming/capabilities.csv # 能力项(为切上下文/模块做输入)
1.2 从事件风暴到上下文映射(Context Mapping)
- 常见关系:Customer--Supplier、Conformist、ACL、Open Host、Published Language、Shared Kernel。
- 明确上游/下游、治理关系、语义边界与演进策略。
上下文映射示意:
Downstream Upstream API/PL 翻译/对齐 ACL 防腐层 Sales
Conformist Catalog
OHS + Published Language
2) 映射到 ABP 模块边界(工程化落地)🏗️
2.1 模块命名与分层(建议 7 层)
约定命名:Company.Product.<Context>.*
。每个上下文建议包含:
Domain.Shared
/Domain
Application.Contracts
/Application
HttpApi
/HttpApi.Client
EntityFrameworkCore
(或MongoDB
)
依赖方向(只允许"向内"):
Web/UI HttpApi Application.Contracts Application Domain EntityFrameworkCore
关键约束:
HttpApi
仅依赖Application.Contracts
(不依Application
实现)。HttpApi.Client
仅依赖Application.Contracts
。- ORM 集成层仅依赖
Domain
。- 严禁跨上下文直连仓储,一律通过 契约(HTTP/消息)。
2.2 ABP CLI:创建解决方案与模块
安装/更新 CLI:
bash
dotnet tool install -g Volo.Abp.Studio.Cli
# 或
dotnet tool update -g Volo.Abp.Studio.Cli
新建解决方案(MVC 示例):
bash
abp new Contoso.SalesSuite -t app -u mvc
为上下文创建 DDD 模块并加入解决方案 (Studio CLI 前缀:abpc):
bash
cd Contoso.SalesSuite
abpc new-module Contoso.Sales -t module:ddd -ts Contoso.SalesSuite.sln
abpc new-module Contoso.Billing -t module:ddd -ts Contoso.SalesSuite.sln
abpc new-module Contoso.Catalog -t module:ddd -ts Contoso.SalesSuite.sln
3) 上下文通信:HttpApi.Client 动态代理 + Polly(重试/熔断)🔗
端点配置(消费者侧 appsettings.json
):
json
{
"RemoteServices": {
"Default": { "BaseUrl": "https://localhost:5001/" },
"Billing": { "BaseUrl": "https://localhost:6001/" }
}
}
注册动态代理 + Polly(关键扩展点 ProxyClientBuildActions
):
csharp
[DependsOn(typeof(AbpHttpClientModule),
typeof(Contoso.Billing.ApplicationContractsModule))]
public class Contoso.BillingClientModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpHttpClientBuilderOptions>(options =>
{
options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) =>
{
var jitter = new Random();
clientBuilder.AddTransientHttpErrorPolicy(pb =>
pb.WaitAndRetryAsync(3, i =>
TimeSpan.FromSeconds(Math.Pow(2, i)) +
TimeSpan.FromMilliseconds(jitter.Next(0, 150))));
});
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(Contoso.Billing.ApplicationContractsModule).Assembly,
remoteServiceConfigurationName: "Billing");
}
}
调用时序图:
🧩 Sales(Consumer) 🤖 HttpApi.Client Proxy 🌐 HTTP 🧩 Billing(HttpApi) 调用 IInvoiceAppService.Get("123") HTTP GET /api/invoices/123 转发至 Controller 200 OK / JSON opt [Retry(指数退避 + 抖动 3 次)] DTO 🧩 Sales(Consumer) 🤖 HttpApi.Client Proxy 🌐 HTTP 🧩 Billing(HttpApi)
4) 一致性:分布式事件总线 + Outbox/Inbox(EF Core)📨
DbContext 接线(最小示例):
csharp
public class SalesDbContext : AbpDbContext<SalesDbContext>, IHasEventOutbox, IHasEventInbox
{
public DbSet<OutgoingEventRecord> OutgoingEvents { get; set; }
public DbSet<IncomingEventRecord> IncomingEvents { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureEventOutbox();
builder.ConfigureEventInbox();
}
}
模块中绑定 Outbox/Inbox 到事件总线:
csharp
public class SalesEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDistributedEventBusOptions>(o =>
{
o.Outboxes.Configure(c => c.UseDbContext<SalesDbContext>());
o.Inboxes.Configure(c => c.UseDbContext<SalesDbContext>());
});
}
}
处理流示意:
写 写 应用服务
Save + Publish 本地事务 业务表 Outbox(OutgoingEvents) Outbox Processor 消息中间件 Consumer Service Inbox(IncomingEvents) 幂等处理
业务逻辑
多实例需配置分布式锁(如 Redis)防止重复并发处理;事件载荷中建议携带
TenantId
,消费端使用using (CurrentTenant.Change(...))
切换。
5) 多租户与数据边界 🏢
服务侧按租户执行:
csharp
public class SalesReportAppService : ApplicationService
{
private readonly IRepository<Order, Guid> _orders;
public SalesReportAppService(IRepository<Order, Guid> orders) => _orders = orders;
public async Task<long> CountOrdersAsync(Guid tenantId)
{
using (CurrentTenant.Change(tenantId))
{
return await _orders.GetCountAsync();
}
}
}
原则:上下文内统一通过
ICurrentTenant
获取租户,严禁跨上下文"越界"读写他域租户数据。
6) 契约门禁(流水线守门员)🛡️
6.1 API 契约:Swashbuckle CLI + oasdiff(检测破坏性变更)
生成 OpenAPI(构建后导出):
bash
dotnet tool install --global Swashbuckle.AspNetCore.Cli
dotnet swagger tofile --output ./artifacts/api.v1.json \
./src/Contoso.Sales.HttpApi.Host/bin/Release/net8.0/Contoso.Sales.HttpApi.Host.dll v1
CI 检查(GitHub Actions 片段):
yaml
- name: Check OpenAPI breaking changes
uses: Tufin/oasdiff-action@v2.1.3
with:
base: './contracts/api/sales.v1.json'
revision: './artifacts/api.v1.json'
check-breaking: true
fail-on-diff: true
6.2 架构门禁:NetArchTest / ArchUnitNET
csharp
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
[Fact]
public void Catalog_Should_Not_Depend_On_Billing_EFCore()
{
var result = Types.InAssemblies(AppDomain.CurrentDomain.GetAssemblies())
.That().ResideInNamespace("Contoso.Catalog", true)
.ShouldNot().HaveDependencyOn("Contoso.Billing.EntityFrameworkCore")
.GetResult();
Assert.True(result.IsSuccessful, string.Join("\n", result.FailingTypeNames));
}
}
CI 编排图:
无破坏 通过 通过 有破坏 违规 失败 开发者提交 PR 🧱 构建/测试 📄 生成 OpenAPI 🔍 oasdiff breaking 🏗️ NetArchTest 规则 🤝 PactNet 合同测试 ✅ 合并 ⛔ 退回并评论
7) 伴随测试(从用例到回归)🧪
7.1 契约测试(PactNet v4 Consumer 示例)------已换为 .WithHttpInteractions()
csharp
var pact = Pact.V4("SalesConsumer", "BillingProvider", new PactConfig { PactDir = "../../../pacts" })
.WithHttpInteractions();
pact.UponReceiving("get invoice")
.WithRequest(HttpMethod.Get, "/api/invoices/123")
.WillRespond()
.WithStatus(HttpStatusCode.OK)
.WithJsonBody(new { id = Match.Type("123"), amount = Match.Decimal(10.5) });
await pact.VerifyAsync(async ctx => {
var client = new HttpClient { BaseAddress = ctx.MockServerUri };
var res = await client.GetAsync("/api/invoices/123");
res.EnsureSuccessStatusCode();
});
7.2 组件/集成:Testcontainers(PostgreSQL/RabbitMQ)
带等待策略与测试集合夹具(复用容器,提速 & 稳定):
csharp
[CollectionDefinition("integration-shared")]
public class IntegrationSharedCollection : ICollectionFixture<SharedContainers> { }
public class SharedContainers : IAsyncLifetime
{
public PostgreSqlContainer Pg { get; private set; } = null!;
public RabbitMqContainer Mq { get; private set; } = null!;
public async Task InitializeAsync()
{
Pg = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
Mq = new RabbitMqBuilder()
.WithImage("rabbitmq:3-management-alpine")
.Build();
await Pg.StartAsync();
await Mq.StartAsync();
}
public async Task DisposeAsync()
{
await Mq.DisposeAsync();
await Pg.DisposeAsync();
}
public string Db => Pg.GetConnectionString();
public string Amqp => Mq.GetConnectionString();
}
8) 分阶段迁移老系统(Strangler Fig)🌿
🧭 Phase 0 盘点
域/能力/耦合图谱 🛡️ Phase 1 围栏
ACL + 只读镜像 🪞 Phase 2 影子/双写
对账+差异报表门禁 🔀 Phase 3 切换
分段接管/回退剧本 📊 发布复盘
破坏率/回滚率/改进项
反模式红线 :共享数据库、跨上下文事务、DTO 当领域模型复用、HttpApi
依赖 Application
实现、跨境引用 *.EntityFrameworkCore
。
9) 可观测与演进度量 📈
- 架构健康度:模块耦合方向稳定性、门禁通过率、契约破坏率。
- 业务健康度:关键事件吞吐/延迟、失败率、回滚率。
- 自动化文档:CI 生成 Context Map / 依赖图;版本附"架构体检报告"。
10) 工程骨架(落地目录)🗂️
/src
/Contoso.Sales.Domain.Shared
/Contoso.Sales.Domain
/Contoso.Sales.Application.Contracts
/Contoso.Sales.Application
/Contoso.Sales.HttpApi
/Contoso.Sales.HttpApi.Client
/Contoso.Sales.EntityFrameworkCore
/Contoso.Billing.(同上)
/Contoso.Catalog.(同上)
/contracts
/api/sales.v1.json
/api/billing.v1.json
/messages/<topic>.schema.json
/quality-gates
/ApiCompat # oasdiff 产物与基线
/ArchRules.Tests # 架构规则测试
/tests
/Sales.AcceptanceTests
/Sales.ComponentTests
/docs
/event-storming/*
/context-map/*