“事件风暴 → 上下文映射 → 模块化”在 ABP vNext 的全链路模板 - 详解

“事件风暴 → 上下文映射 → 模块化”在 ABP vNext 的全链路模板 ✨



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 PictureProcess/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

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");
  }
  }

调用时序图

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 接线(最小示例)

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>());
      });
      }
      }

处理流示意

应用服务
Save + Publish
本地事务
业务表
Outbox(OutgoingEvents)
Outbox Processor
消息中间件
Consumer Service
Inbox(IncomingEvents)
幂等处理
业务逻辑

多实例需配置分布式锁(如 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 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

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()

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)

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/.schema.json
/quality-gates
  /ApiCompat          # oasdiff 产物与基线
  /ArchRules.Tests    # 架构规则测试
/tests
  /Sales.AcceptanceTests
  /Sales.ComponentTests
/docs
  /event-storming/*
  /context-map/*
posted @ 2025-11-07 12:50  yangykaifa  阅读(5)  评论(0)    收藏  举报