“事件风暴 → 上下文映射 → 模块化”在 ABP vNext 的全链路模板
“事件风暴 → 上下文映射 → 模块化”在 ABP vNext 的全链路模板 ✨
📚 目录
- “事件风暴 → 上下文映射 → 模块化”在 ABP vNext 的全链路模板 ✨
- 0) 摘要(TL;DR)📝
- 1) 工作坊与产出物 🤝
- 1.1 事件风暴(Event Storming)
- 1.2 从事件风暴到上下文映射(Context Mapping)
- 2) 映射到 ABP 模块边界(工程化落地)🏗️
- 2.1 模块命名与分层(建议 7 层)
- 2.2 ABP CLI:创建解决方案与模块
- 3) 上下文通信:HttpApi.Client 动态代理 + Polly(重试/熔断)🔗
- 4) 一致性:分布式事件总线 + Outbox/Inbox(EF Core)📨
- 5) 多租户与数据边界 🏢
- 6) 契约门禁(流水线守门员)🛡️
- 6.1 API 契约:Swashbuckle CLI + oasdiff(检测破坏性变更)
- 6.2 架构门禁:NetArchTest / ArchUnitNET
- 7) 伴随测试(从用例到回归)🧪
- 7.1 契约测试(PactNet v4 Consumer 示例)——**已换为 `.WithHttpInteractions()`**
- 7.2 组件/集成:Testcontainers(PostgreSQL/RabbitMQ)
- 8) 分阶段迁移老系统(Strangler Fig)🌿
- 9) 可观测与演进度量 📈
- 10) 工程骨架(落地目录)🗂️
0) 摘要(TL;DR)📝
本文交付一套从业务共创工作坊到可运行工程骨架的闭环:
事件风暴 → 子域/上下文划分 → 上下文映射(关系/协作模式) → ABP 模块边界与依赖矩阵 → 契约门禁(CI) → 伴随测试 → 分阶段迁移老系统 → 持续度量与反模式清单。
总览图(从白板到上线):
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。
- 明确上游/下游、治理关系、语义边界与演进策略。
上下文映射示意:
2) 映射到 ABP 模块边界(工程化落地)🏗️
2.1 模块命名与分层(建议 7 层)
约定命名:Company.Product.<Context>.*
。每个上下文建议包含:
Domain.Shared
/Domain
Application.Contracts
/Application
HttpApi
/HttpApi.Client
EntityFrameworkCore
(或MongoDB
)
依赖方向(只允许“向内”):
关键约束:
HttpApi
仅依赖Application.Contracts
(不依Application
实现)。HttpApi.Client
仅依赖Application.Contracts
。- ORM 集成层仅依赖
Domain
。- 严禁跨上下文直连仓储,一律通过 契约(HTTP/消息)。
2.2 ABP CLI:创建解决方案与模块
安装/更新 CLI:
dotnet tool install -g Volo.Abp.Studio.Cli
# 或
dotnet tool update -g Volo.Abp.Studio.Cli
新建解决方案(MVC 示例):
abp new Contoso.SalesSuite -t app -u mvc
为上下文创建 DDD 模块并加入解决方案(Studio CLI 前缀:abpc):
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
):
{"RemoteServices": {"Default": { "BaseUrl": "https://localhost:5001/" },"Billing": { "BaseUrl": "https://localhost:6001/" }}
}
注册动态代理 + Polly(关键扩展点 ProxyClientBuildActions
):
[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");}
}
调用时序图:
4) 一致性:分布式事件总线 + Outbox/Inbox(EF Core)📨
DbContext 接线(最小示例):
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 到事件总线:
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>());});}
}
处理流示意:
多实例需配置分布式锁(如 Redis)防止重复并发处理;事件载荷中建议携带
TenantId
,消费端使用using (CurrentTenant.Change(...))
切换。
5) 多租户与数据边界 🏢
服务侧按租户执行:
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(构建后导出):
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 片段):
- name: Check OpenAPI breaking changesuses: Tufin/oasdiff-action@v2.1.3with:base: './contracts/api/sales.v1.json'revision: './artifacts/api.v1.json'check-breaking: truefail-on-diff: true
6.2 架构门禁:NetArchTest / ArchUnitNET
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 编排图:
7) 伴随测试(从用例到回归)🧪
7.1 契约测试(PactNet v4 Consumer 示例)——已换为 .WithHttpInteractions()
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)
带等待策略与测试集合夹具(复用容器,提速 & 稳定):
[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)🌿
反模式红线:共享数据库、跨上下文事务、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/*