From 017e689abd54b2e4cfdc72ff56f84d8291e4f9e9 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 14:20:50 +0800
Subject: [PATCH 1/7] =?UTF-8?q?feat(cqrs):=20=E8=A1=A5=E9=BD=90=E8=AF=B7?=
=?UTF-8?q?=E6=B1=82=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=9F=BA=E5=87=86?=
=?UTF-8?q?=E7=9F=A9=E9=98=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 request handler Singleton 与 Transient 生命周期 benchmark,并说明 Scoped 对照的宿主前置条件
- 更新 benchmark README,补充当前覆盖范围与后续扩展方向
- 更新 cqrs-rewrite active tracking 与 trace,记录 RP-092 验证结果和沙箱外 benchmark 权威结论
---
.../Messaging/RequestLifetimeBenchmarks.cs | 221 ++++++++++++++++++
GFramework.Cqrs.Benchmarks/README.md | 8 +-
.../todos/cqrs-rewrite-migration-tracking.md | 29 ++-
.../traces/cqrs-rewrite-migration-trace.md | 45 ++++
4 files changed, 293 insertions(+), 10 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs
new file mode 100644
index 00000000..735705db
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs
@@ -0,0 +1,221 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Order;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Core.Ioc;
+using GFramework.Core.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 对比 request steady-state dispatch 在不同 handler 生命周期下的额外开销。
+///
+///
+/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
+/// `Scoped` 在两个 runtime 中都依赖显式作用域边界,而当前 benchmark 宿主故意保持“单根容器最小宿主”模型,
+/// 直接把 scoped 解析压到根作用域会让对照语义失真,因此留到未来有真实 scoped host 基线时再扩展。
+///
+[Config(typeof(Config))]
+public class RequestLifetimeBenchmarks
+{
+ private MicrosoftDiContainer _container = null!;
+ private ICqrsRuntime _runtime = null!;
+ private ServiceProvider _serviceProvider = null!;
+ private IMediator _mediatr = null!;
+ private BenchmarkRequestHandler _baselineHandler = null!;
+ private BenchmarkRequest _request = null!;
+
+ ///
+ /// 控制当前 benchmark 使用的 handler 生命周期。
+ ///
+ [Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
+ public HandlerLifetime Lifetime { get; set; }
+
+ ///
+ /// 可公平比较的 benchmark handler 生命周期集合。
+ ///
+ public enum HandlerLifetime
+ {
+ ///
+ /// 复用单个 handler 实例。
+ ///
+ Singleton,
+
+ ///
+ /// 每次分发都重新解析新的 handler 实例。
+ ///
+ Transient
+ }
+
+ ///
+ /// 配置 request lifetime benchmark 的公共输出格式。
+ ///
+ private sealed class Config : ManualConfig
+ {
+ public Config()
+ {
+ AddJob(Job.Default);
+ AddColumnProvider(DefaultColumnProviders.Instance);
+ AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestLifetime"));
+ AddDiagnoser(MemoryDiagnoser.Default);
+ WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
+ }
+ }
+
+ ///
+ /// 构建当前生命周期下的 GFramework 与 MediatR request 对照宿主。
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
+ {
+ MinLevel = LogLevel.Fatal
+ };
+ Fixture.Setup($"RequestLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
+
+ _baselineHandler = new BenchmarkRequestHandler();
+ _request = new BenchmarkRequest(Guid.NewGuid());
+
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ RegisterGFrameworkHandler(container, Lifetime);
+ });
+ _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
+ _container,
+ LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestLifetimeBenchmarks) + "." + Lifetime));
+
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(RequestLifetimeBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkRequestHandler),
+ ResolveMediatRLifetime(Lifetime));
+ _mediatr = _serviceProvider.GetRequiredService();
+ }
+
+ ///
+ /// 释放当前生命周期矩阵持有的 benchmark 宿主资源。
+ ///
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
+ }
+
+ ///
+ /// 直接调用 handler,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
+ ///
+ [Benchmark(Baseline = true)]
+ public ValueTask SendRequest_Baseline()
+ {
+ return _baselineHandler.Handle(_request, CancellationToken.None);
+ }
+
+ ///
+ /// 通过 GFramework.CQRS runtime 发送 request。
+ ///
+ [Benchmark]
+ public ValueTask SendRequest_GFrameworkCqrs()
+ {
+ return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None);
+ }
+
+ ///
+ /// 通过 MediatR 发送 request,作为外部对照。
+ ///
+ [Benchmark]
+ public Task SendRequest_MediatR()
+ {
+ return _mediatr.Send(_request, CancellationToken.None);
+ }
+
+ ///
+ /// 按生命周期把 benchmark request handler 注册到 GFramework 容器。
+ ///
+ /// 当前 benchmark 拥有并负责释放的容器。
+ /// 待比较的 handler 生命周期。
+ private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ switch (lifetime)
+ {
+ case HandlerLifetime.Singleton:
+ container.RegisterSingleton, BenchmarkRequestHandler>();
+ return;
+
+ case HandlerLifetime.Transient:
+ container.RegisterTransient, BenchmarkRequestHandler>();
+ return;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
+ }
+ }
+
+ ///
+ /// 将 benchmark 生命周期映射为 MediatR 组装所需的 。
+ ///
+ /// 待比较的 handler 生命周期。
+ private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
+ {
+ return lifetime switch
+ {
+ HandlerLifetime.Singleton => ServiceLifetime.Singleton,
+ HandlerLifetime.Transient => ServiceLifetime.Transient,
+ _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
+ };
+ }
+
+ ///
+ /// Benchmark request。
+ ///
+ /// 请求标识。
+ public sealed record BenchmarkRequest(Guid Id) :
+ GFramework.Cqrs.Abstractions.Cqrs.IRequest,
+ MediatR.IRequest;
+
+ ///
+ /// Benchmark response。
+ ///
+ /// 响应标识。
+ public sealed record BenchmarkResponse(Guid Id);
+
+ ///
+ /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
+ ///
+ public sealed class BenchmarkRequestHandler :
+ GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler,
+ MediatR.IRequestHandler
+ {
+ ///
+ /// 处理 GFramework.CQRS request。
+ ///
+ public ValueTask Handle(BenchmarkRequest request, CancellationToken cancellationToken)
+ {
+ return ValueTask.FromResult(new BenchmarkResponse(request.Id));
+ }
+
+ ///
+ /// 处理 MediatR request。
+ ///
+ Task MediatR.IRequestHandler.Handle(
+ BenchmarkRequest request,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new BenchmarkResponse(request.Id));
+ }
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index 3ac5fccb..51682881 100644
--- a/GFramework.Cqrs.Benchmarks/README.md
+++ b/GFramework.Cqrs.Benchmarks/README.md
@@ -16,6 +16,8 @@
- 运行前输出并校验场景配置
- `Messaging/RequestBenchmarks.cs`
- direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
+- `Messaging/RequestLifetimeBenchmarks.cs`
+ - `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestStartupBenchmarks.cs`
@@ -39,7 +41,7 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
## 后续扩展方向
-- generated invoker provider 与纯反射 dispatch 对比
-- generated stream invoker provider 与纯反射建流对比
-- registration / service lifetime 矩阵
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
+- stream handler 生命周期矩阵
+- 带真实显式作用域边界的 scoped host 对照
+- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index 2583af84..c2029a9a 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,9 +7,9 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-091`
+- 恢复点编号:`CQRS-REWRITE-RP-092`
- 当前阶段:`Phase 8`
-- 当前 PR 锚点:`PR #331`
+- 当前 PR 锚点:`待创建(当前分支 feat/cqrs-optimization 尚未为 RP-092 建立新 PR)`
- 当前结论:
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归
@@ -27,17 +27,21 @@ CQRS 迁移与收敛。
- 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径
- 当前 `RP-090` 已收敛 `PR #326` benchmark review:统一 benchmark 最小宿主构建、冻结 GFramework 容器、限制 MediatR 扫描范围,并恢复 request startup cold-start 对照
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
-- `ai-plan` active 入口现以 `RP-091` 为最新恢复锚点;`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
+- `ai-plan` active 入口现以 `RP-092` 为最新恢复锚点;`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
-- 当前分支为 `fix/package-validation-guard`
+- 当前分支为 `feat/cqrs-optimization`
+- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`2c58d8b6`, 2026-05-07 13:24:46 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线
- `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外
+- `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
- latest-head review 现仍有少量 open thread,但本地复核后,仍成立的问题已收敛到 benchmark 对照公平性、workflow 输入安全性与 active 文档压缩
- benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型
- `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered`
+- `BenchmarkDotNet` 在当前 agent 沙箱里会因自动生成的 bootstrap 脚本异常失败;同一 `dotnet run --no-build` 命令在沙箱外执行通过,因此本轮以沙箱外结果作为 benchmark 权威验证
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
@@ -47,6 +51,7 @@ CQRS 迁移与收敛。
- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证
- 若后续新增 benchmark / example / tooling 项目但未同步校验发布面,solution 级 `dotnet pack` 仍可能在 tag 发布前才暴露异常包
- `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness
+- 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
@@ -76,12 +81,22 @@ CQRS 迁移与收敛。
- 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行
- `git diff --check`
- 结果:通过
+- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过(以沙箱外 `--no-build` 权威结果为准)
+ - 备注:`Singleton` 下 baseline / MediatR / GFramework 均值约 `5.633 ns / 58.687 ns / 301.731 ns`;`Transient` 下约 `5.044 ns / 52.274 ns / 287.863 ns`
+- `python3 scripts/license-header.py --check`
+ - 结果:通过
+ - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE`
+- `git diff --check`
+ - 结果:通过
## 下一推荐步骤
-1. 运行 `dotnet pack` 与新的 `scripts/validate-packed-modules.sh`,确认本轮共享校验脚本与 PR workflow 步骤在本地一致通过
-2. 运行受影响的 Release build / 头部校验,确认 workflow 与脚本改动未引入新的命名、文件头或 shell 语法问题
-3. 创建修复 PR 时,将重点放在“发布面保护前移到 PR”而不是“扩充 expected package 列表”
+1. 若继续沿用 `$gframework-batch-boot 50`,优先补 `stream handler` 生命周期矩阵,与当前 request 生命周期切片保持对称
+2. 若要扩到 `Scoped` 生命周期,对 benchmark 宿主先补显式 scope 基线,而不是直接在根容器上解析 scoped handler
+3. 若后续继续跑 BenchmarkDotNet,本地 agent 环境优先直接使用沙箱外命令,避免再次命中自动生成脚本在沙箱内的 bootstrap 异常
## 活跃文档
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 6bf3ce36..50fb8956 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -1,5 +1,50 @@
# CQRS 重写迁移追踪
+## 2026-05-07
+
+### 阶段:request handler 生命周期矩阵 benchmark(CQRS-REWRITE-RP-092)
+
+- 使用 `$gframework-batch-boot 50` 启动本轮批次,并按技能要求先复核 `origin/main` 基线与 branch diff:
+ - `origin/main` = `2c58d8b6`,提交时间 `2026-05-07 13:24:46 +0800`
+ - 本地 `main` = `c2d22285`,已落后于 remote-tracking ref,因此不作为本轮 batch baseline
+ - 当前 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 在开工前为 `0 files / 0 lines`
+- 本轮批次目标:继续推进 `GFramework.Cqrs.Benchmarks`,补一个独立、低风险、可单项目 Release 验证的 request 生命周期对照切片
+- 主线程先复核现有 benchmark 宿主与 runtime 解析路径后确认:
+ - `RequestBenchmarks` 与 `StreamingBenchmarks` 当前都固定使用单根容器宿主
+ - `MicrosoftDiContainer` 虽支持 `RegisterScoped` / `CreateScope()`,但当前 `CqrsDispatcher` 的 steady-state benchmark 路径直接从根容器解析 handler
+ - 因此若直接把 `Scoped` 注册加入现有 benchmark,会把“根作用域下的 scoped 解析”误当成公平对照,语义不成立
+- 本轮决策:
+ - 新增 `Messaging/RequestLifetimeBenchmarks.cs`
+ - 生命周期矩阵只覆盖 `Singleton / Transient`
+ - 在 XML 文档与 README 中显式注明:`Scoped` 需要等未来具备真实显式作用域边界的 benchmark host 后再比较
+- 已修改:
+ - `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs`
+ - `GFramework.Cqrs.Benchmarks/README.md`
+ - `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`
+ - `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
+- 预期结果:
+ - `GFramework.Cqrs.Benchmarks` 不再只覆盖“有无 generated provider / startup / pipeline”的维度,也开始覆盖 request steady-state 下的 handler 生命周期成本差异
+ - benchmark 设计继续保持“只加入语义公平的矩阵”,避免把作用域模型不对称的结论写进基线
+
+### 验证(RP-092)
+
+- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过(沙箱外权威结果)
+ - 备注:当前 agent 沙箱内执行同一 benchmark 会在 BenchmarkDotNet 自动生成 bootstrap 阶段失败;切换到沙箱外后,`restore/build` 自举与 6 个 benchmark case 全部通过
+ - 备注:`Singleton` 下 baseline / MediatR / GFramework 分别约 `5.633 ns / 58.687 ns / 301.731 ns`
+ - 备注:`Transient` 下 baseline / MediatR / GFramework 分别约 `5.044 ns / 52.274 ns / 287.863 ns`
+- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
+
+### 当前下一步(RP-092)
+
+1. 若 branch diff 仍明显低于 `$gframework-batch-boot 50` 阈值,下一批优先补 `stream handler` 生命周期矩阵,保持 request / stream benchmark 维度对称
+2. 若准备扩到 `Scoped` 生命周期,先为 benchmark host 设计真实显式作用域基线,再进入运行时对照
+
## 2026-05-06
### 阶段:PR #331 review 收尾补丁(CQRS-REWRITE-RP-091)
From d7293aa4754865fc8e44b166c3071f96be07503c Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 17:20:14 +0800
Subject: [PATCH 2/7] =?UTF-8?q?refactor(core):=20=E7=BB=9F=E4=B8=80?=
=?UTF-8?q?=E6=97=A7=E7=89=88=E5=91=BD=E4=BB=A4=E6=9F=A5=E8=AF=A2=E5=88=B0?=
=?UTF-8?q?Cqrs=E8=BF=90=E8=A1=8C=E6=97=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 重构 Core 兼容命令查询入口,使 legacy SendCommand/SendQuery 通过内部 bridge request 复用统一 CQRS runtime
- 新增 legacy bridge handler 与真实启动路径回归测试,验证默认架构初始化会自动接入统一 pipeline
- 更新 Core 与 CQRS 文档及 cqrs-rewrite 跟踪,记录 Mediator 尚未吸收的能力差距与后续收口方向
---
.../Architectures/ArchitectureContextTests.cs | 120 +++++++++++++++
.../ArchitectureModulesBehaviorTests.cs | 53 +++++++
.../LegacyArchitectureBridgeAsyncQuery.cs | 28 ++++
.../LegacyArchitectureBridgeCommand.cs | 33 ++++
...gacyArchitectureBridgeCommandWithResult.cs | 28 ++++
.../LegacyArchitectureBridgeQuery.cs | 28 ++++
.../LegacyBridgePipelineTracker.cs | 41 +++++
.../LegacyBridgeTrackingPipelineBehavior.cs | 24 +++
.../Architectures/ArchitectureContext.cs | 54 ++++---
GFramework.Core/Command/CommandExecutor.cs | 141 +++++++++++++++++-
.../Cqrs/LegacyAsyncCommandDispatchRequest.cs | 19 +++
...egacyAsyncCommandDispatchRequestHandler.cs | 24 +++
...LegacyAsyncCommandResultDispatchRequest.cs | 20 +++
...syncCommandResultDispatchRequestHandler.cs | 23 +++
.../Cqrs/LegacyAsyncQueryDispatchRequest.cs | 20 +++
.../LegacyAsyncQueryDispatchRequestHandler.cs | 23 +++
.../Cqrs/LegacyCommandDispatchRequest.cs | 19 +++
.../LegacyCommandDispatchRequestHandler.cs | 22 +++
.../LegacyCommandResultDispatchRequest.cs | 20 +++
...gacyCommandResultDispatchRequestHandler.cs | 21 +++
.../Cqrs/LegacyCqrsDispatchHandlerBase.cs | 28 ++++
.../Cqrs/LegacyCqrsDispatchRequestBase.cs | 15 ++
.../Cqrs/LegacyQueryDispatchRequest.cs | 20 +++
.../Cqrs/LegacyQueryDispatchRequestHandler.cs | 21 +++
GFramework.Core/Query/AsyncQueryExecutor.cs | 67 ++++++++-
GFramework.Core/Query/QueryExecutor.cs | 57 ++++++-
GFramework.Core/README.md | 3 +
.../Modules/AsyncQueryExecutorModule.cs | 6 +-
.../Services/Modules/CommandExecutorModule.cs | 6 +-
.../Services/Modules/QueryExecutorModule.cs | 6 +-
.../todos/cqrs-rewrite-migration-tracking.md | 29 +++-
.../traces/cqrs-rewrite-migration-trace.md | 47 ++++++
docs/zh-CN/core/command.md | 3 +
docs/zh-CN/core/context.md | 4 +
docs/zh-CN/core/cqrs.md | 10 ++
docs/zh-CN/core/query.md | 3 +
36 files changed, 1042 insertions(+), 44 deletions(-)
create mode 100644 GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs
create mode 100644 GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs
create mode 100644 GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs
create mode 100644 GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs
create mode 100644 GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
create mode 100644 GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs
create mode 100644 GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
create mode 100644 GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs
create mode 100644 GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs
create mode 100644 GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs
create mode 100644 GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
create mode 100644 GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCommandDispatchRequestHandler.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCommandResultDispatchRequestHandler.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
create mode 100644 GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
create mode 100644 GFramework.Core/Cqrs/LegacyQueryDispatchRequestHandler.cs
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
index 47ad0d2c..4c33f725 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
@@ -15,6 +15,7 @@ using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Query;
+using GFramework.Core.Services.Modules;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
@@ -71,6 +72,8 @@ public class ArchitectureContextTests
_container.RegisterPlurality(_queryBus);
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
+ new CqrsRuntimeModule().Register(_container);
+ RegisterLegacyBridgeHandlers(_container);
_context = new ArchitectureContext(_container);
}
@@ -124,6 +127,24 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(42));
}
+ ///
+ /// 测试 legacy 查询通过 发送时会进入统一 CQRS pipeline,
+ /// 并把当前架构上下文注入到查询对象。
+ ///
+ [Test]
+ public void SendQuery_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testQuery = new LegacyArchitectureBridgeQuery();
+ var bridgeContext = CreateFrozenBridgeContext();
+
+ var result = bridgeContext.SendQuery(testQuery);
+
+ Assert.That(result, Is.EqualTo(24));
+ Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+
///
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
///
@@ -146,6 +167,24 @@ public class ArchitectureContextTests
Assert.That(testCommand.Executed, Is.True);
}
+ ///
+ /// 测试 legacy 命令通过 发送时会进入统一 CQRS pipeline,
+ /// 并把当前架构上下文注入到命令对象。
+ ///
+ [Test]
+ public void SendCommand_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testCommand = new LegacyArchitectureBridgeCommand();
+ var bridgeContext = CreateFrozenBridgeContext();
+
+ bridgeContext.SendCommand(testCommand);
+
+ Assert.That(testCommand.Executed, Is.True);
+ Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+
///
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
///
@@ -168,6 +207,87 @@ public class ArchitectureContextTests
Assert.That(result, Is.EqualTo(123));
}
+ ///
+ /// 测试 legacy 带返回值命令通过 发送时会进入统一 CQRS pipeline,
+ /// 并保持原始返回值语义。
+ ///
+ [Test]
+ public void SendCommand_WithResult_Should_Bridge_Through_CqrsRuntime()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testCommand = new LegacyArchitectureBridgeCommandWithResult();
+ var bridgeContext = CreateFrozenBridgeContext();
+
+ var result = bridgeContext.SendCommand(testCommand);
+
+ Assert.That(result, Is.EqualTo(42));
+ Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+
+ ///
+ /// 测试 legacy 异步查询通过 发送时也会进入统一 CQRS pipeline。
+ ///
+ [Test]
+ public async Task SendQueryAsync_Should_Bridge_Through_CqrsRuntime_And_Preserve_Context()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var testQuery = new LegacyArchitectureBridgeAsyncQuery();
+ var bridgeContext = CreateFrozenBridgeContext();
+
+ var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
+
+ Assert.That(result, Is.EqualTo(64));
+ Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+
+ ///
+ /// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
+ ///
+ /// 能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。
+ private static ArchitectureContext CreateFrozenBridgeContext()
+ {
+ var container = new MicrosoftDiContainer();
+ RegisterLegacyBridgeHandlers(container);
+ new CqrsRuntimeModule().Register(container);
+ container.ExecuteServicesHook(services =>
+ services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>)));
+ container.Freeze();
+ return new ArchitectureContext(container);
+ }
+
+ ///
+ /// 通过反射把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
+ ///
+ /// 目标测试容器。
+ private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ string[] handlerTypeNames =
+ [
+ "LegacyCommandDispatchRequestHandler",
+ "LegacyCommandResultDispatchRequestHandler",
+ "LegacyAsyncCommandDispatchRequestHandler",
+ "LegacyAsyncCommandResultDispatchRequestHandler",
+ "LegacyQueryDispatchRequestHandler",
+ "LegacyAsyncQueryDispatchRequestHandler"
+ ];
+
+ var coreAssembly = typeof(ArchitectureContext).Assembly;
+
+ foreach (var handlerTypeName in handlerTypeNames)
+ {
+ var handlerType = coreAssembly.GetType($"GFramework.Core.Cqrs.{handlerTypeName}")
+ ?? throw new InvalidOperationException($"Bridge handler type '{handlerTypeName}' was not found.");
+ var handlerInstance = Activator.CreateInstance(handlerType)
+ ?? throw new InvalidOperationException(
+ $"Bridge handler type '{handlerType.FullName}' could not be instantiated.");
+ container.RegisterPlurality(handlerInstance);
+ }
+ }
+
///
/// 测试SendCommand方法(带返回值)在命令为null时应抛出ArgumentNullException
///
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
index b9e7b180..ff9880f5 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
@@ -6,6 +6,8 @@ using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@@ -80,6 +82,36 @@ public class ArchitectureModulesBehaviorTests
await architecture.DestroyAsync();
}
+ ///
+ /// 验证默认架构初始化路径会自动扫描 Core 程序集里的 legacy bridge handler,
+ /// 使旧 SendCommand / SendQuery 入口也能进入统一 CQRS pipeline。
+ ///
+ [Test]
+ public async Task InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ var architecture = new LegacyBridgeArchitecture();
+
+ await architecture.InitializeAsync();
+
+ var query = new LegacyArchitectureBridgeQuery();
+ var command = new LegacyArchitectureBridgeCommand();
+
+ var queryResult = architecture.Context.SendQuery(query);
+ architecture.Context.SendCommand(command);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(queryResult, Is.EqualTo(24));
+ Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
+ Assert.That(command.Executed, Is.True);
+ Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
+ });
+
+ await architecture.DestroyAsync();
+ }
+
///
/// 用于测试模块行为的最小架构实现。
///
@@ -94,6 +126,27 @@ public class ArchitectureModulesBehaviorTests
}
}
+ ///
+ /// 通过公开初始化入口注册测试 pipeline behavior 的最小架构,
+ /// 用于验证默认 Core 程序集扫描是否会自动接入 legacy bridge handler。
+ ///
+ private sealed class LegacyBridgeArchitecture : Architecture
+ {
+ ///
+ /// 在容器钩子阶段注册 open-generic pipeline behavior,
+ /// 以便 bridge request 走真实的架构初始化与 handler 自动扫描链路。
+ ///
+ public override Action? Configurator => services =>
+ services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LegacyBridgeTrackingPipelineBehavior<,>));
+
+ ///
+ /// 保持空初始化,让测试只聚焦默认 CQRS 接线与 legacy bridge handler 自动发现。
+ ///
+ protected override void OnInitialize()
+ {
+ }
+ }
+
///
/// 记录模块安装调用情况的测试模块。
///
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs
new file mode 100644
index 00000000..864864a9
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeAsyncQuery.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Query;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 用于验证 legacy 异步查询桥接时也会显式注入当前架构上下文。
+///
+public sealed class LegacyArchitectureBridgeAsyncQuery : ContextAwareBase, IAsyncQuery
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 执行异步查询并返回测试结果。
+ ///
+ public Task DoAsync()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return Task.FromResult(64);
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs
new file mode 100644
index 00000000..f58d646b
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommand.cs
@@ -0,0 +1,33 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Command;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 用于验证 legacy 命令桥接时会把当前 注入到命令对象。
+///
+public sealed class LegacyArchitectureBridgeCommand : ContextAwareBase, ICommand
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 获取当前命令是否已经执行。
+ ///
+ public bool Executed { get; private set; }
+
+ ///
+ /// 执行命令并记录 bridge handler 注入的上下文。
+ ///
+ public void Execute()
+ {
+ Executed = true;
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs
new file mode 100644
index 00000000..d97d553a
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeCommandWithResult.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Command;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 用于验证 legacy 带返回值命令桥接时会沿用统一 runtime。
+///
+public sealed class LegacyArchitectureBridgeCommandWithResult : ContextAwareBase, ICommand
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 执行命令并返回测试结果。
+ ///
+ public int Execute()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return 42;
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs
new file mode 100644
index 00000000..1f7617a6
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyArchitectureBridgeQuery.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Query;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 用于验证 legacy 查询桥接时会把当前 注入到查询对象。
+///
+public sealed class LegacyArchitectureBridgeQuery : ContextAwareBase, IQuery
+{
+ ///
+ /// 获取执行期间观察到的上下文实例。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 执行查询并返回测试结果。
+ ///
+ public int Do()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return 24;
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
new file mode 100644
index 00000000..27ce3d4e
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Threading;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
+///
+public static class LegacyBridgePipelineTracker
+{
+ private static int _invocationCount;
+
+ ///
+ /// 获取当前进程内被识别为 legacy bridge request 的 pipeline 命中次数。
+ ///
+ public static int InvocationCount => Volatile.Read(ref _invocationCount);
+
+ ///
+ /// 重置计数器。
+ ///
+ public static void Reset()
+ {
+ Volatile.Write(ref _invocationCount, 0);
+ }
+
+ ///
+ /// 若当前请求类型属于 Core legacy bridge request,则记录一次命中。
+ ///
+ public static void Record(Type requestType)
+ {
+ ArgumentNullException.ThrowIfNull(requestType);
+
+ if (string.Equals(requestType.Namespace, "GFramework.Core.Cqrs", StringComparison.Ordinal) &&
+ requestType.Name.Contains("Legacy", StringComparison.Ordinal))
+ {
+ Interlocked.Increment(ref _invocationCount);
+ }
+ }
+}
diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs b/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs
new file mode 100644
index 00000000..4e719e0e
--- /dev/null
+++ b/GFramework.Core.Tests/Architectures/LegacyBridgeTrackingPipelineBehavior.cs
@@ -0,0 +1,24 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Threading;
+using GFramework.Cqrs.Abstractions.Cqrs;
+
+namespace GFramework.Core.Tests.Architectures;
+
+///
+/// 记录 legacy Core CQRS bridge request 是否经过统一 CQRS pipeline 的测试行为。
+///
+public sealed class LegacyBridgeTrackingPipelineBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ ///
+ public async ValueTask Handle(
+ TRequest message,
+ MessageHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ LegacyBridgePipelineTracker.Record(typeof(TRequest));
+ return await next(message, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs
index b63a0427..e143c957 100644
--- a/GFramework.Core/Architectures/ArchitectureContext.cs
+++ b/GFramework.Core/Architectures/ArchitectureContext.cs
@@ -11,6 +11,7 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
+using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
@@ -180,10 +181,12 @@ public class ArchitectureContext : IArchitectureContext
/// 查询结果
public TResult SendQuery(IQuery query)
{
- if (query == null) throw new ArgumentNullException(nameof(query));
- var queryBus = GetOrCache();
- if (queryBus == null) throw new InvalidOperationException("IQueryExecutor not registered");
- return queryBus.Send(query);
+ ArgumentNullException.ThrowIfNull(query);
+ var boxedResult = SendRequest(
+ new LegacyQueryDispatchRequest(
+ query,
+ () => query.Do()));
+ return (TResult)boxedResult!;
}
///
@@ -192,7 +195,7 @@ public class ArchitectureContext : IArchitectureContext
/// 查询响应类型
/// 要发送的查询对象
/// 查询结果
- public TResponse SendQuery(Cqrs.Abstractions.Cqrs.Query.IQuery query)
+ public TResponse SendQuery(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query)
{
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
}
@@ -205,10 +208,13 @@ public class ArchitectureContext : IArchitectureContext
/// 查询结果
public async Task SendQueryAsync(IAsyncQuery query)
{
- if (query == null) throw new ArgumentNullException(nameof(query));
- var asyncQueryBus = GetOrCache();
- if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
- return await asyncQueryBus.SendAsync(query).ConfigureAwait(false);
+ ArgumentNullException.ThrowIfNull(query);
+ var boxedResult = await SendRequestAsync(
+ new LegacyAsyncQueryDispatchRequest(
+ query,
+ async () => await query.DoAsync().ConfigureAwait(false)))
+ .ConfigureAwait(false);
+ return (TResult)boxedResult!;
}
///
@@ -218,7 +224,7 @@ public class ArchitectureContext : IArchitectureContext
/// 要发送的查询对象
/// 取消令牌,用于取消操作
/// 包含查询结果的ValueTask
- public async ValueTask SendQueryAsync(Cqrs.Abstractions.Cqrs.Query.IQuery query,
+ public async ValueTask SendQueryAsync(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
@@ -355,7 +361,7 @@ public class ArchitectureContext : IArchitectureContext
/// 取消令牌,用于取消操作
/// 包含命令执行结果的ValueTask
public async ValueTask SendCommandAsync(
- Cqrs.Abstractions.Cqrs.Command.ICommand command,
+ global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(command);
@@ -369,9 +375,7 @@ public class ArchitectureContext : IArchitectureContext
public async Task SendCommandAsync(IAsyncCommand command)
{
ArgumentNullException.ThrowIfNull(command);
- var commandBus = GetOrCache();
- if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
- await commandBus.SendAsync(command).ConfigureAwait(false);
+ await SendRequestAsync(new LegacyAsyncCommandDispatchRequest(command)).ConfigureAwait(false);
}
///
@@ -383,9 +387,12 @@ public class ArchitectureContext : IArchitectureContext
public async Task SendCommandAsync(IAsyncCommand command)
{
ArgumentNullException.ThrowIfNull(command);
- var commandBus = GetOrCache();
- if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
- return await commandBus.SendAsync(command).ConfigureAwait(false);
+ var boxedResult = await SendRequestAsync(
+ new LegacyAsyncCommandResultDispatchRequest(
+ command,
+ async () => await command.ExecuteAsync().ConfigureAwait(false)))
+ .ConfigureAwait(false);
+ return (TResult)boxedResult!;
}
///
@@ -394,7 +401,7 @@ public class ArchitectureContext : IArchitectureContext
/// 命令响应类型
/// 要发送的命令对象
/// 命令执行结果
- public TResponse SendCommand(Cqrs.Abstractions.Cqrs.Command.ICommand command)
+ public TResponse SendCommand(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command)
{
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
}
@@ -406,8 +413,7 @@ public class ArchitectureContext : IArchitectureContext
public void SendCommand(ICommand command)
{
ArgumentNullException.ThrowIfNull(command);
- var commandBus = GetOrCache();
- commandBus.Send(command);
+ SendRequest(new LegacyCommandDispatchRequest(command));
}
///
@@ -419,9 +425,11 @@ public class ArchitectureContext : IArchitectureContext
public TResult SendCommand(ICommand command)
{
ArgumentNullException.ThrowIfNull(command);
- var commandBus = GetOrCache();
- if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
- return commandBus.Send(command);
+ var boxedResult = SendRequest(
+ new LegacyCommandResultDispatchRequest(
+ command,
+ () => command.Execute()));
+ return (TResult)boxedResult!;
}
#endregion
diff --git a/GFramework.Core/Command/CommandExecutor.cs b/GFramework.Core/Command/CommandExecutor.cs
index 2d2d7e32..355379e3 100644
--- a/GFramework.Core/Command/CommandExecutor.cs
+++ b/GFramework.Core/Command/CommandExecutor.cs
@@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Abstractions.Command;
+using GFramework.Core.Abstractions.Rule;
+using GFramework.Core.Cqrs;
+using GFramework.Cqrs.Abstractions.Cqrs;
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
namespace GFramework.Core.Command;
@@ -10,8 +13,20 @@ namespace GFramework.Core.Command;
/// 表示一个命令执行器,用于执行命令操作。
/// 该类实现了 ICommandExecutor 接口,提供命令执行的核心功能。
///
-public sealed class CommandExecutor : ICommandExecutor
+public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExecutor
{
+ private readonly ICqrsRuntime? _runtime = runtime;
+
+ ///
+ /// 获取当前执行器是否已接入统一 CQRS runtime。
+ ///
+ ///
+ /// 当调用方只是直接 new 一个执行器做纯单元测试时,这里允许为空,并回退到 legacy 直接执行路径;
+ /// 当执行器由架构容器提供给 使用时,应始终传入 runtime,
+ /// 以便旧入口也复用统一 pipeline 与 handler 调度链路。
+ ///
+ public bool UsesCqrsRuntime => _runtime is not null;
+
///
/// 发送并执行无返回值的命令
///
@@ -21,6 +36,11 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
+ if (TryExecuteThroughCqrsRuntime(command, static currentCommand => new LegacyCommandDispatchRequest(currentCommand)))
+ {
+ return;
+ }
+
command.Execute();
}
@@ -35,6 +55,16 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
+ if (TryExecuteThroughCqrsRuntime(
+ command,
+ static currentCommand => new LegacyCommandResultDispatchRequest(
+ currentCommand,
+ () => currentCommand.Execute()),
+ out TResult? result))
+ {
+ return result!;
+ }
+
return command.Execute();
}
@@ -47,6 +77,11 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
+ if (TryResolveDispatchContext(command, out var context) && _runtime is not null)
+ {
+ return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
+ }
+
return command.ExecuteAsync();
}
@@ -61,6 +96,108 @@ public sealed class CommandExecutor : ICommandExecutor
{
ArgumentNullException.ThrowIfNull(command);
+ if (TryResolveDispatchContext(command, out var context) && _runtime is not null)
+ {
+ return BridgeAsyncCommandWithResultAsync(context, command);
+ }
+
return command.ExecuteAsync();
}
-}
\ No newline at end of file
+
+ ///
+ /// 尝试通过统一 CQRS runtime 执行当前 legacy 请求。
+ ///
+ /// legacy 目标对象类型。
+ /// bridge request 类型。
+ /// 即将执行的 legacy 目标对象。
+ /// 用于创建 bridge request 的工厂。
+ /// 若成功切入 CQRS runtime 则返回 ;否则返回 。
+ private bool TryExecuteThroughCqrsRuntime(
+ TTarget target,
+ Func requestFactory)
+ where TTarget : class
+ where TRequest : IRequest
+ {
+ if (!TryResolveDispatchContext(target, out var context) || _runtime is null)
+ {
+ return false;
+ }
+
+ _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
+ return true;
+ }
+
+ ///
+ /// 尝试通过统一 CQRS runtime 执行当前 legacy 请求,并返回装箱结果。
+ ///
+ /// legacy 目标对象类型。
+ /// 预期结果类型。
+ /// bridge request 类型。
+ /// 即将执行的 legacy 目标对象。
+ /// 用于创建 bridge request 的工厂。
+ /// 若命中 bridge,则返回执行结果;否则返回默认值。
+ /// 若成功切入 CQRS runtime 则返回 ;否则返回 。
+ private bool TryExecuteThroughCqrsRuntime(
+ TTarget target,
+ Func requestFactory,
+ out TResult? result)
+ where TTarget : class
+ where TRequest : IRequest
-public sealed class QueryExecutor : IQueryExecutor
+public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
{
+ private readonly ICqrsRuntime? _runtime = runtime;
+
+ ///
+ /// 获取当前执行器是否已接入统一 CQRS runtime。
+ ///
+ public bool UsesCqrsRuntime => _runtime is not null;
+
///
/// 执行指定的查询并返回结果。
- /// 该方法通过调用查询对象的 Do 方法来获取结果。
+ /// 当查询对象携带可用的架构上下文且执行器已接入统一 runtime 时,
+ /// 该方法会先把 legacy 查询包装成内部 request 并交给 ,
+ /// 以复用统一的 dispatch / pipeline 入口;否则回退到 legacy 直接执行。
///
/// 查询结果的类型。
/// 要执行的查询对象,必须实现 IQuery<TResult> 接口。
/// 查询执行的结果,类型为 TResult。
public TResult Send(IQuery query)
{
- // 验证查询参数不为 null,如果为 null 则抛出 ArgumentNullException 异常
ArgumentNullException.ThrowIfNull(query);
- // 调用查询对象的 Do 方法执行查询并返回结果
+ if (TryResolveDispatchContext(query, out var context) && _runtime is not null)
+ {
+ var boxedResult = _runtime.SendAsync(
+ context,
+ new LegacyQueryDispatchRequest(
+ query,
+ () => query.Do()))
+ .AsTask()
+ .GetAwaiter()
+ .GetResult();
+ return (TResult)boxedResult!;
+ }
+
return query.Do();
}
+
+ ///
+ /// 解析当前 legacy 查询应该绑定到哪个架构上下文。
+ ///
+ /// 即将执行的 legacy 查询对象。
+ /// 命中时返回可用于 CQRS runtime 的架构上下文。
+ /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 。
+ private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ {
+ context = null!;
+
+ if (_runtime is null || query is not IContextAware contextAware)
+ {
+ return false;
+ }
+
+ try
+ {
+ context = contextAware.GetContext();
+ return true;
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+ }
}
diff --git a/GFramework.Core/README.md b/GFramework.Core/README.md
index 93481ab3..39def5dd 100644
--- a/GFramework.Core/README.md
+++ b/GFramework.Core/README.md
@@ -15,6 +15,9 @@
- 资源、对象池、日志、协程、并发、环境、配置与本地化
- 服务模块管理、时间提供器与默认的 IoC 容器适配
+标准架构启动路径下,旧 `Command` / `Query` 兼容入口现在会继续保持原有使用方式,
+但底层会通过 `GFramework.Cqrs` 的统一 runtime、pipeline 与上下文注入链路执行。
+
它不负责:
- 游戏内容配置、Scene / UI / Storage 等游戏层能力
diff --git a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs
index 9fb6f787..e1a0524d 100644
--- a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs
+++ b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs
@@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Query;
+using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@@ -35,7 +36,8 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
/// 依赖注入容器实例。
public void Register(IIocContainer container)
{
- container.RegisterPlurality(new AsyncQueryExecutor());
+ ArgumentNullException.ThrowIfNull(container);
+ container.RegisterPlurality(new AsyncQueryExecutor(container.Get()));
}
///
@@ -55,4 +57,4 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Core/Services/Modules/CommandExecutorModule.cs b/GFramework.Core/Services/Modules/CommandExecutorModule.cs
index 2ea60049..3d327cda 100644
--- a/GFramework.Core/Services/Modules/CommandExecutorModule.cs
+++ b/GFramework.Core/Services/Modules/CommandExecutorModule.cs
@@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Command;
+using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@@ -35,7 +36,8 @@ public sealed class CommandExecutorModule : IServiceModule
/// 依赖注入容器实例。
public void Register(IIocContainer container)
{
- container.RegisterPlurality(new CommandExecutor());
+ ArgumentNullException.ThrowIfNull(container);
+ container.RegisterPlurality(new CommandExecutor(container.Get()));
}
///
@@ -55,4 +57,4 @@ public sealed class CommandExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Core/Services/Modules/QueryExecutorModule.cs b/GFramework.Core/Services/Modules/QueryExecutorModule.cs
index 1f3e8403..40e1eb91 100644
--- a/GFramework.Core/Services/Modules/QueryExecutorModule.cs
+++ b/GFramework.Core/Services/Modules/QueryExecutorModule.cs
@@ -4,6 +4,7 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Query;
+using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Services.Modules;
@@ -35,7 +36,8 @@ public sealed class QueryExecutorModule : IServiceModule
/// 依赖注入容器实例。
public void Register(IIocContainer container)
{
- container.RegisterPlurality(new QueryExecutor());
+ ArgumentNullException.ThrowIfNull(container);
+ container.RegisterPlurality(new QueryExecutor(container.Get()));
}
///
@@ -55,4 +57,4 @@ public sealed class QueryExecutorModule : IServiceModule
{
return ValueTask.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index c2029a9a..0d6ed018 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-092`
+- 恢复点编号:`CQRS-REWRITE-RP-093`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`待创建(当前分支 feat/cqrs-optimization 尚未为 RP-092 建立新 PR)`
- 当前结论:
@@ -27,15 +27,21 @@ CQRS 迁移与收敛。
- 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径
- 当前 `RP-090` 已收敛 `PR #326` benchmark review:统一 benchmark 最小宿主构建、冻结 GFramework 容器、限制 MediatR 扫描范围,并恢复 request startup cold-start 对照
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
- - 当前 `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
-- `ai-plan` active 入口现以 `RP-092` 为最新恢复锚点;`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
+ - 当前 `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
+- `ai-plan` active 入口现以 `RP-093` 为最新恢复锚点;`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`2c58d8b6`, 2026-05-07 13:24:46 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线
+- 当前分支相对 `origin/main` 的累计 branch diff 为 `4 files / 303 lines`,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
- `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外
- `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵
+- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime`
+- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
+- `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
+- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
- latest-head review 现仍有少量 open thread,但本地复核后,仍成立的问题已收敛到 benchmark 对照公平性、workflow 输入安全性与 active 文档压缩
@@ -54,6 +60,7 @@ CQRS 迁移与收敛。
- 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
+- legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline;若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成”
## 最近权威验证
@@ -91,12 +98,22 @@ CQRS 迁移与收敛。
- 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE`
- `git diff --check`
- 结果:通过
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
+ - 结果:通过,`45/45` passed
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
+ - 结果:通过,`1644/1644` passed
+- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
## 下一推荐步骤
-1. 若继续沿用 `$gframework-batch-boot 50`,优先补 `stream handler` 生命周期矩阵,与当前 request 生命周期切片保持对称
-2. 若要扩到 `Scoped` 生命周期,对 benchmark 宿主先补显式 scope 基线,而不是直接在根容器上解析 scoped handler
-3. 若后续继续跑 BenchmarkDotNet,本地 agent 环境优先直接使用沙箱外命令,避免再次命中自动生成脚本在沙箱内的 bootstrap 异常
+1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进
+2. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责
+3. 若回到 benchmark 方向,优先补 `stream handler` 生命周期矩阵;若要扩到 `Scoped` 生命周期,先为 benchmark 宿主设计真实显式 scope 基线
## 活跃文档
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 50fb8956..76264060 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,53 @@
## 2026-05-07
+### 阶段:legacy Core CQRS -> GFramework.Cqrs bridge(CQRS-REWRITE-RP-093)
+
+- 延续 `$gframework-batch-boot 50`,本轮明确不只盯 benchmark,而是同时处理两个目标:
+ - 复核 `ai-libs/Mediator` 还有哪些能力尚未被 `GFramework.Cqrs` 吸收
+ - 验证 `GFramework.Core` 的简单 `Command` / `Query` 兼容入口能否在不改外部用法的前提下,底层统一改走 `GFramework.Cqrs`
+- 主线程先完成 `GFramework.Core` bridge 实现收尾与测试修正:
+ - `ArchitectureContext` 的 legacy `SendCommand(...)` / `SendQuery(...)` / `SendQueryAsync(...)` 现在会创建内部 bridge request,并直接通过统一 `ICqrsRuntime` 分发
+ - `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 在解析到 runtime 且目标对象可提供架构上下文时,也会复用同一条 bridge/runtime 路径
+ - 为避免破坏不依赖容器的旧测试,执行器仍保留“未接入 runtime 时直接执行”的回退语义
+- 新增 `GFramework.Core/Cqrs/Legacy*DispatchRequest*.cs` 与对应 handler,把 legacy 命令/查询包装成内部 request:
+ - bridge handler 在执行前会显式把当前 `IArchitectureContext` 注入给 `IContextAware` 目标
+ - 这让旧调用链在不改 public API 的情况下,也能复用统一 pipeline 与 handler dispatch 语义
+- 生产接线结论已经本地复核:
+ - `CqrsRuntimeModule` 只注册 runtime / registrar / registration service,本身不直接手工注册 bridge handler
+ - 默认生产路径依赖 `ArchitectureBootstrapper.ConfigureServices(...)` 自动调用 `RegisterCqrsHandlersFromAssemblies([architectureType.Assembly, typeof(ArchitectureContext).Assembly])`
+ - 因此 `GFramework.Core` 程序集中的 internal bridge handler 会在标准架构初始化阶段自动被扫描和注册,不需要业务侧手工补注册
+- 为防止以后有人改坏默认扫描范围,本轮额外补了一条更接近真实启动路径的回归:
+ - `ArchitectureModulesBehaviorTests.InitializeAsync_Should_AutoRegister_LegacyBridgeHandlers_For_Default_Core_Assemblies`
+ - 该用例通过 `Architecture.Configurator` 注册 open-generic pipeline behavior,然后直接走 `Architecture.InitializeAsync()`,验证旧 `SendCommand` / `SendQuery` 兼容入口能命中统一 pipeline
+- 只读 subagent 同步完成 `Mediator` 差距复核,接受的结论是六类未完全吸收能力:
+ - `IMediator` / `ISender` / `IPublisher` 风格 facade
+ - telemetry / tracing / metrics
+ - stream pipeline
+ - notification publisher 策略
+ - 生成器配置与诊断公开面
+ - 生命周期 / 缓存公开配置面
+- 文档与恢复入口同步更新:
+ - `docs/zh-CN/core/context.md`、`command.md`、`query.md`、`cqrs.md`
+ - `GFramework.Core/README.md`
+ - active tracking / trace 升级到 `RP-093`
+
+### 验证(RP-093)
+
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
+ - 结果:通过,`45/45` passed
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
+ - 结果:通过,`1644/1644` passed
+- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
+
+### 当前下一步(RP-093)
+
+1. 若继续沿用 `$gframework-batch-boot 50`,优先从 `stream pipeline` 或 `notification publisher` 策略中切一块对齐 `Mediator`
+2. 若要继续收敛 public seam,下一批优先设计 facade,而不是继续扩大 `ArchitectureContext` 的兼容职责
+
### 阶段:request handler 生命周期矩阵 benchmark(CQRS-REWRITE-RP-092)
- 使用 `$gframework-batch-boot 50` 启动本轮批次,并按技能要求先复核 `origin/main` 基线与 branch diff:
diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md
index f5667568..49e74611 100644
--- a/docs/zh-CN/core/command.md
+++ b/docs/zh-CN/core/command.md
@@ -105,6 +105,9 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
- `SendCommandAsync(IAsyncCommand)`
- `SendCommandAsync(IAsyncCommand)`
+在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`。
+这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
+
在 `IContextAware` 对象内,通常直接通过扩展使用:
```csharp
diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md
index 40cf1fe5..128e27ee 100644
--- a/docs/zh-CN/core/context.md
+++ b/docs/zh-CN/core/context.md
@@ -111,6 +111,10 @@ this.SendEvent(new PlayerDiedEvent());
这部分入口主要用于兼容存量代码。新功能优先看 [cqrs](./cqrs.md)。
+在标准 `Architecture` 初始化路径里,这些旧入口现在会复用同一个 `ICqrsRuntime`:
+旧 `SendCommand(...)` / `SendQuery(...)` 仍保持原有调用方式,但会经过统一的 request pipeline 与上下文注入链路。
+只有在你直接 `new CommandExecutor()`、`new QueryExecutor()` 做隔离测试,且没有提供 runtime 时,才会回退到 legacy 直接执行。
+
## 新 CQRS 入口
`IArchitectureContext` 也是当前 CQRS runtime 的主入口。最重要的方法是:
diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md
index 2a62250d..389bd6cc 100644
--- a/docs/zh-CN/core/cqrs.md
+++ b/docs/zh-CN/core/cqrs.md
@@ -224,6 +224,7 @@ RegisterCqrsPipelineBehavior>();
这里有两个边界需要分开理解:
- 旧 `Command` / `Query` 入口仍可用于维护历史调用链
+- 标准 `Architecture` 启动路径下,旧入口现在会通过内部 bridge request 复用同一个 `ICqrsRuntime`
- 旧命名空间下的 `ICqrsRuntime` 只是为了兼容既有引用而保留的 alias;面向新代码时,应直接使用
`GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`
@@ -232,6 +233,15 @@ RegisterCqrsPipelineBehavior>();
- 在维护历史代码:允许继续使用旧 Command / Query
- 在写新功能或新模块:优先使用 CQRS
+相对 `ai-libs/Mediator`,当前 `GFramework.Cqrs` 仍有几类能力差距尚未完全吸收:
+
+- `IMediator` / `ISender` / `IPublisher` 风格的一等 facade 公开入口
+- telemetry / tracing / metrics 的运行时与生成器配置面
+- 独立的 stream pipeline 行为体系
+- 更丰富的 notification publisher 策略
+- 更强的生成器配置与诊断公开面
+- 生命周期 / 缓存策略的显式公开配置面
+
## 源码阅读入口
如果你需要直接回到源码确认 CQRS 契约,建议按下面这几组入口阅读:
diff --git a/docs/zh-CN/core/query.md b/docs/zh-CN/core/query.md
index 10e58ac9..e01cde2f 100644
--- a/docs/zh-CN/core/query.md
+++ b/docs/zh-CN/core/query.md
@@ -83,6 +83,9 @@ var count = this.SendQuery(
- `SendQuery(IQuery)`
- `SendQueryAsync(IAsyncQuery)`
+在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`。
+因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。
+
在 `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
```csharp
From 6056159866aaf4e0cd425b46e319133e412aebaf Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 17:54:05 +0800
Subject: [PATCH 3/7] =?UTF-8?q?fix(core):=20=E6=94=B6=E5=8F=A3=20legacy=20?=
=?UTF-8?q?cqrs=20bridge=20=E8=AF=84=E5=AE=A1=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 legacy bridge 测试装配与清理流程,改用 InternalsVisibleTo 和显式 handler 注册,补齐共享计数器重置与生命周期说明
- 优化 CommandExecutor、QueryExecutor 与相关模块的 runtime 契约,补充 XML 文档、nullable 注解和显式依赖解析
- 更新 legacy 异步 bridge 的取消语义、兼容文档回退边界以及 cqrs-rewrite active tracking/trace
---
.../Architectures/ArchitectureContextTests.cs | 43 ++++++-----
.../ArchitectureModulesBehaviorTests.cs | 75 +++++++++++--------
.../LegacyBridgePipelineTracker.cs | 10 ++-
GFramework.Core/Command/CommandExecutor.cs | 22 ++++--
.../Cqrs/LegacyAsyncCommandDispatchRequest.cs | 10 ++-
...LegacyAsyncCommandResultDispatchRequest.cs | 3 +
...syncCommandResultDispatchRequestHandler.cs | 4 +-
.../Cqrs/LegacyAsyncQueryDispatchRequest.cs | 3 +
.../LegacyAsyncQueryDispatchRequestHandler.cs | 4 +-
.../Cqrs/LegacyCommandDispatchRequest.cs | 10 ++-
.../LegacyCommandResultDispatchRequest.cs | 3 +
.../Cqrs/LegacyCqrsDispatchHandlerBase.cs | 5 ++
.../Cqrs/LegacyCqrsDispatchRequestBase.cs | 1 +
.../Cqrs/LegacyQueryDispatchRequest.cs | 3 +
GFramework.Core/Properties/AssemblyInfo.cs | 6 ++
GFramework.Core/Query/AsyncQueryExecutor.cs | 16 ++--
GFramework.Core/Query/QueryExecutor.cs | 8 +-
.../Modules/AsyncQueryExecutorModule.cs | 12 ++-
.../Services/Modules/CommandExecutorModule.cs | 12 ++-
.../Services/Modules/QueryExecutorModule.cs | 12 ++-
.../todos/cqrs-rewrite-migration-tracking.md | 33 ++++++--
.../traces/cqrs-rewrite-migration-trace.md | 32 ++++++++
docs/zh-CN/core/command.md | 1 +
docs/zh-CN/core/context.md | 2 +-
24 files changed, 240 insertions(+), 90 deletions(-)
create mode 100644 GFramework.Core/Properties/AssemblyInfo.cs
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
index 4c33f725..835930d0 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
@@ -10,6 +10,7 @@ using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Architectures;
using GFramework.Core.Command;
+using GFramework.Core.Cqrs;
using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
@@ -45,6 +46,9 @@ namespace GFramework.Core.Tests.Architectures;
[TestFixture]
public class ArchitectureContextTests
{
+ ///
+ /// 初始化测试所需的容器与默认服务实例。
+ ///
[SetUp]
public void SetUp()
{
@@ -78,6 +82,16 @@ public class ArchitectureContextTests
_context = new ArchitectureContext(_container);
}
+ ///
+ /// 释放当前测试创建的容器,并清理 legacy bridge 共享计数状态。
+ ///
+ [TearDown]
+ public void TearDown()
+ {
+ LegacyBridgePipelineTracker.Reset();
+ _container?.Dispose();
+ }
+
private AsyncQueryExecutor? _asyncQueryBus;
private CommandExecutor? _commandBus;
private MicrosoftDiContainer? _container;
@@ -258,34 +272,19 @@ public class ArchitectureContextTests
}
///
- /// 通过反射把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
+ /// 把 GFramework.Core 内部的 legacy bridge handler 实例预先注册成可见的实例绑定。
///
/// 目标测试容器。
private static void RegisterLegacyBridgeHandlers(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
- string[] handlerTypeNames =
- [
- "LegacyCommandDispatchRequestHandler",
- "LegacyCommandResultDispatchRequestHandler",
- "LegacyAsyncCommandDispatchRequestHandler",
- "LegacyAsyncCommandResultDispatchRequestHandler",
- "LegacyQueryDispatchRequestHandler",
- "LegacyAsyncQueryDispatchRequestHandler"
- ];
-
- var coreAssembly = typeof(ArchitectureContext).Assembly;
-
- foreach (var handlerTypeName in handlerTypeNames)
- {
- var handlerType = coreAssembly.GetType($"GFramework.Core.Cqrs.{handlerTypeName}")
- ?? throw new InvalidOperationException($"Bridge handler type '{handlerTypeName}' was not found.");
- var handlerInstance = Activator.CreateInstance(handlerType)
- ?? throw new InvalidOperationException(
- $"Bridge handler type '{handlerType.FullName}' could not be instantiated.");
- container.RegisterPlurality(handlerInstance);
- }
+ container.RegisterPlurality(new LegacyCommandDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyCommandResultDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyAsyncCommandDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyAsyncCommandResultDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyQueryDispatchRequestHandler());
+ container.RegisterPlurality(new LegacyAsyncQueryDispatchRequestHandler());
}
///
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
index ff9880f5..4c0b86f8 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
@@ -37,6 +37,7 @@ public class ArchitectureModulesBehaviorTests
{
GameContext.Clear();
TrackingPipelineBehavior.InvocationCount = 0;
+ LegacyBridgePipelineTracker.Reset();
}
///
@@ -49,15 +50,19 @@ public class ArchitectureModulesBehaviorTests
var architecture = new ModuleTestArchitecture(target => target.InstallModule(module));
await architecture.InitializeAsync();
-
- Assert.Multiple(() =>
+ try
{
- Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
- Assert.That(module.InstallCallCount, Is.EqualTo(1));
- Assert.That(architecture.Context.GetUtility(), Is.Not.Null);
- });
-
- await architecture.DestroyAsync();
+ Assert.Multiple(() =>
+ {
+ Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
+ Assert.That(module.InstallCallCount, Is.EqualTo(1));
+ Assert.That(architecture.Context.GetUtility(), Is.Not.Null);
+ });
+ }
+ finally
+ {
+ await architecture.DestroyAsync();
+ }
}
///
@@ -70,16 +75,20 @@ public class ArchitectureModulesBehaviorTests
target.RegisterCqrsPipelineBehavior>());
await architecture.InitializeAsync();
-
- var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
-
- Assert.Multiple(() =>
+ try
{
- Assert.That(response, Is.EqualTo("handled"));
- Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1));
- });
+ var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
- await architecture.DestroyAsync();
+ Assert.Multiple(() =>
+ {
+ Assert.That(response, Is.EqualTo("handled"));
+ Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1));
+ });
+ }
+ finally
+ {
+ await architecture.DestroyAsync();
+ }
}
///
@@ -93,23 +102,27 @@ public class ArchitectureModulesBehaviorTests
var architecture = new LegacyBridgeArchitecture();
await architecture.InitializeAsync();
-
- var query = new LegacyArchitectureBridgeQuery();
- var command = new LegacyArchitectureBridgeCommand();
-
- var queryResult = architecture.Context.SendQuery(query);
- architecture.Context.SendCommand(command);
-
- Assert.Multiple(() =>
+ try
{
- Assert.That(queryResult, Is.EqualTo(24));
- Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
- Assert.That(command.Executed, Is.True);
- Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
- Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
- });
+ var query = new LegacyArchitectureBridgeQuery();
+ var command = new LegacyArchitectureBridgeCommand();
- await architecture.DestroyAsync();
+ var queryResult = architecture.Context.SendQuery(query);
+ architecture.Context.SendCommand(command);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(queryResult, Is.EqualTo(24));
+ Assert.That(query.ObservedContext, Is.SameAs(architecture.Context));
+ Assert.That(command.Executed, Is.True);
+ Assert.That(command.ObservedContext, Is.SameAs(architecture.Context));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(2));
+ });
+ }
+ finally
+ {
+ await architecture.DestroyAsync();
+ }
}
///
diff --git a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
index 27ce3d4e..599e8277 100644
--- a/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
+++ b/GFramework.Core.Tests/Architectures/LegacyBridgePipelineTracker.cs
@@ -2,12 +2,19 @@
// SPDX-License-Identifier: Apache-2.0
using System.Threading;
+using GFramework.Core.Cqrs;
namespace GFramework.Core.Tests.Architectures;
///
/// 为 legacy bridge pipeline 回归测试保存跨泛型闭包共享的计数状态。
///
+///
+/// 该计数器通过 原子递增,并使用
+/// 读写,因此单次读写操作本身是线程安全的。
+/// 由于状态在同一进程内跨 fixture 共享,所有使用它的测试都必须在清理阶段调用 ,
+/// 以避免并行或失败测试把旧计数泄露给后续断言。
+///
public static class LegacyBridgePipelineTracker
{
private static int _invocationCount;
@@ -32,8 +39,7 @@ public static class LegacyBridgePipelineTracker
{
ArgumentNullException.ThrowIfNull(requestType);
- if (string.Equals(requestType.Namespace, "GFramework.Core.Cqrs", StringComparison.Ordinal) &&
- requestType.Name.Contains("Legacy", StringComparison.Ordinal))
+ if (typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType))
{
Interlocked.Increment(ref _invocationCount);
}
diff --git a/GFramework.Core/Command/CommandExecutor.cs b/GFramework.Core/Command/CommandExecutor.cs
index 355379e3..fd401c9d 100644
--- a/GFramework.Core/Command/CommandExecutor.cs
+++ b/GFramework.Core/Command/CommandExecutor.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
+using System.Diagnostics.CodeAnalysis;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
@@ -77,7 +78,7 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
{
ArgumentNullException.ThrowIfNull(command);
- if (TryResolveDispatchContext(command, out var context) && _runtime is not null)
+ if (TryResolveDispatchContext(command, out var context))
{
return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
}
@@ -96,9 +97,9 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
{
ArgumentNullException.ThrowIfNull(command);
- if (TryResolveDispatchContext(command, out var context) && _runtime is not null)
+ if (TryResolveDispatchContext(command, out var context))
{
- return BridgeAsyncCommandWithResultAsync(context, command);
+ return BridgeAsyncCommandWithResultAsync(_runtime, context, command);
}
return command.ExecuteAsync();
@@ -118,7 +119,7 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
where TTarget : class
where TRequest : IRequest
{
- if (!TryResolveDispatchContext(target, out var context) || _runtime is null)
+ if (!TryResolveDispatchContext(target, out var context))
{
return false;
}
@@ -144,7 +145,7 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
where TTarget : class
where TRequest : IRequest
{
- if (!TryResolveDispatchContext(target, out var context) || _runtime is null)
+ if (!TryResolveDispatchContext(target, out var context))
{
result = default;
return false;
@@ -159,14 +160,16 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
/// 通过统一 CQRS runtime 异步执行 legacy 带返回值命令,并把装箱结果还原为目标类型。
///
/// 命令返回值类型。
+ /// 负责调度当前 bridge request 的统一 CQRS runtime。
/// 当前架构上下文。
/// 要桥接的 legacy 命令。
/// 命令执行结果。
- private async Task BridgeAsyncCommandWithResultAsync(
+ private static async Task BridgeAsyncCommandWithResultAsync(
+ ICqrsRuntime runtime,
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncCommand command)
{
- var boxedResult = await _runtime!.SendAsync(
+ var boxedResult = await runtime.SendAsync(
context,
new LegacyAsyncCommandResultDispatchRequest(
command,
@@ -181,7 +184,10 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
/// 即将执行的 legacy 目标对象。
/// 命中时返回可用于 CQRS runtime 的架构上下文。
/// 如果既接入了 runtime 且目标对象提供了上下文,则返回 。
- private bool TryResolveDispatchContext(object target, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ [MemberNotNullWhen(true, nameof(_runtime))]
+ private bool TryResolveDispatchContext(
+ object target,
+ out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
context = null!;
diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
index b69f369b..882f5295 100644
--- a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
+++ b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequest.cs
@@ -9,11 +9,17 @@ namespace GFramework.Core.Cqrs;
///
/// 包装 legacy 异步无返回值命令,使其能够通过自有 CQRS runtime 调度。
///
+/// 当前 bridge request 代理的 legacy 异步命令实例。
internal sealed class LegacyAsyncCommandDispatchRequest(CoreCommand.IAsyncCommand command)
- : LegacyCqrsDispatchRequestBase(command), IRequest
+ : LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest
{
///
/// 获取当前 bridge request 代理的异步命令实例。
///
- public CoreCommand.IAsyncCommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command));
+ public CoreCommand.IAsyncCommand Command { get; } = command;
+
+ private static CoreCommand.IAsyncCommand ValidateCommand(CoreCommand.IAsyncCommand command)
+ {
+ return command ?? throw new ArgumentNullException(nameof(command));
+ }
}
diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs
index 18f4cd5d..fe374f30 100644
--- a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs
+++ b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequest.cs
@@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
///
/// 包装 legacy 异步带返回值命令,使其能够通过自有 CQRS runtime 调度。
///
+/// 需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。
+/// 封装 legacy 异步命令执行逻辑并返回装箱结果的委托。
internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Func> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest
{
@@ -16,5 +18,6 @@ internal sealed class LegacyAsyncCommandResultDispatchRequest(object target, Fun
///
/// 异步执行底层 legacy 命令并返回装箱后的结果。
///
+ /// 表示异步执行结果的任务;任务结果为底层 legacy 命令返回的装箱值。
public Task ExecuteAsync() => _executeAsync();
}
diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs
index d58a661f..8296cd38 100644
--- a/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs
+++ b/GFramework.Core/Cqrs/LegacyAsyncCommandResultDispatchRequestHandler.cs
@@ -17,7 +17,9 @@ internal sealed class LegacyAsyncCommandResultDispatchRequestHandler
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
+ // Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
+ cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Target);
- return await request.ExecuteAsync().ConfigureAwait(false);
+ return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
}
}
diff --git a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
index 113a3a3b..679d37b2 100644
--- a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
+++ b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequest.cs
@@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
///
/// 包装 legacy 异步查询,使其能够通过自有 CQRS runtime 调度。
///
+/// 需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。
+/// 封装 legacy 异步查询执行逻辑并返回装箱结果的委托。
internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func> executeAsync)
: LegacyCqrsDispatchRequestBase(target), IRequest
{
@@ -16,5 +18,6 @@ internal sealed class LegacyAsyncQueryDispatchRequest(object target, Func
/// 异步执行底层 legacy 查询并返回装箱后的结果。
///
+ /// 表示异步执行结果的任务;任务结果为底层 legacy 查询返回的装箱值。
public Task ExecuteAsync() => _executeAsync();
}
diff --git a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs
index b9bc848e..6aa48590 100644
--- a/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs
+++ b/GFramework.Core/Cqrs/LegacyAsyncQueryDispatchRequestHandler.cs
@@ -17,7 +17,9 @@ internal sealed class LegacyAsyncQueryDispatchRequestHandler
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
+ // Legacy DoAsync contract does not accept CancellationToken; use WaitAsync so the caller can observe cancellation promptly.
+ cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Target);
- return await request.ExecuteAsync().ConfigureAwait(false);
+ return await request.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
}
}
diff --git a/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
index bbb66713..9010de14 100644
--- a/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
+++ b/GFramework.Core/Cqrs/LegacyCommandDispatchRequest.cs
@@ -9,11 +9,17 @@ namespace GFramework.Core.Cqrs;
///
/// 包装 legacy 无返回值命令,使其能够通过自有 CQRS runtime 调度。
///
+/// 当前 bridge request 代理的 legacy 命令实例。
internal sealed class LegacyCommandDispatchRequest(CoreCommand.ICommand command)
- : LegacyCqrsDispatchRequestBase(command), IRequest
+ : LegacyCqrsDispatchRequestBase(ValidateCommand(command)), IRequest
{
///
/// 获取当前 bridge request 代理的命令实例。
///
- public CoreCommand.ICommand Command { get; } = command ?? throw new ArgumentNullException(nameof(command));
+ public CoreCommand.ICommand Command { get; } = command;
+
+ private static CoreCommand.ICommand ValidateCommand(CoreCommand.ICommand command)
+ {
+ return command ?? throw new ArgumentNullException(nameof(command));
+ }
}
diff --git a/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
index e4c2de40..d6e03342 100644
--- a/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
+++ b/GFramework.Core/Cqrs/LegacyCommandResultDispatchRequest.cs
@@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
///
/// 包装 legacy 带返回值命令,使其能够通过自有 CQRS runtime 调度。
///
+/// 需要在 bridge handler 中接收上下文注入的 legacy 命令目标实例。
+/// 封装 legacy 命令执行逻辑并返回装箱结果的委托。
internal sealed class LegacyCommandResultDispatchRequest(object target, Func execute)
: LegacyCqrsDispatchRequestBase(target), IRequest
{
@@ -16,5 +18,6 @@ internal sealed class LegacyCommandResultDispatchRequest(object target, Func
/// 执行底层 legacy 命令并返回装箱后的结果。
///
+ /// 底层 legacy 命令执行后的装箱结果;若命令语义无返回值则为 。
public object? Execute() => _execute();
}
diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
index 1745465f..d2fabccb 100644
--- a/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
+++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchHandlerBase.cs
@@ -14,6 +14,11 @@ internal abstract class LegacyCqrsDispatchHandlerBase : ContextAwareBase
///
/// 在执行 legacy 命令或查询前,把当前架构上下文显式注入给支持 的目标对象。
///
+ /// 即将执行的 legacy 目标对象。
+ /// 为 。
+ ///
+ /// 目标对象实现了 ,但当前 handler 还没有可用的架构上下文。
+ ///
protected void PrepareTarget(object target)
{
ArgumentNullException.ThrowIfNull(target);
diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
index 11370c95..8e0d6dfc 100644
--- a/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
+++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchRequestBase.cs
@@ -6,6 +6,7 @@ namespace GFramework.Core.Cqrs;
///
/// 为 legacy Command / Query 到自有 CQRS runtime 的桥接请求提供共享的目标对象封装。
///
+/// 需要在 bridge handler 中接收上下文注入的 legacy 目标对象。
internal abstract class LegacyCqrsDispatchRequestBase(object target)
{
///
diff --git a/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs b/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
index 4eb2c15f..d87cf99f 100644
--- a/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
+++ b/GFramework.Core/Cqrs/LegacyQueryDispatchRequest.cs
@@ -8,6 +8,8 @@ namespace GFramework.Core.Cqrs;
///
/// 包装 legacy 同步查询,使其能够通过自有 CQRS runtime 调度。
///
+/// 需要在 bridge handler 中接收上下文注入的 legacy 查询目标实例。
+/// 封装 legacy 查询执行逻辑并返回装箱结果的委托。
internal sealed class LegacyQueryDispatchRequest(object target, Func execute)
: LegacyCqrsDispatchRequestBase(target), IRequest
{
@@ -16,5 +18,6 @@ internal sealed class LegacyQueryDispatchRequest(object target, Func ex
///
/// 执行底层 legacy 查询并返回装箱后的结果。
///
+ /// 底层 legacy 查询执行后的装箱结果;若查询无返回值则为 。
public object? Execute() => _execute();
}
diff --git a/GFramework.Core/Properties/AssemblyInfo.cs b/GFramework.Core/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..cbf402ed
--- /dev/null
+++ b/GFramework.Core/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("GFramework.Core.Tests")]
diff --git a/GFramework.Core/Query/AsyncQueryExecutor.cs b/GFramework.Core/Query/AsyncQueryExecutor.cs
index e1b04300..f59b63f9 100644
--- a/GFramework.Core/Query/AsyncQueryExecutor.cs
+++ b/GFramework.Core/Query/AsyncQueryExecutor.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
+using System.Diagnostics.CodeAnalysis;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
@@ -30,9 +31,9 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
{
ArgumentNullException.ThrowIfNull(query);
- if (TryResolveDispatchContext(query, out var context) && _runtime is not null)
+ if (TryResolveDispatchContext(query, out var context))
{
- return BridgeAsyncQueryAsync(context, query);
+ return BridgeAsyncQueryAsync(_runtime, context, query);
}
return query.DoAsync();
@@ -42,14 +43,16 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
/// 通过统一 CQRS runtime 异步执行 legacy 查询,并把装箱结果还原为目标类型。
///
/// 查询结果类型。
+ /// 负责调度当前 bridge request 的统一 CQRS runtime。
/// 当前架构上下文。
/// 要桥接的 legacy 查询。
/// 查询执行结果。
- private async Task BridgeAsyncQueryAsync(
+ private static async Task BridgeAsyncQueryAsync(
+ ICqrsRuntime runtime,
GFramework.Core.Abstractions.Architectures.IArchitectureContext context,
IAsyncQuery query)
{
- var boxedResult = await _runtime!.SendAsync(
+ var boxedResult = await runtime.SendAsync(
context,
new LegacyAsyncQueryDispatchRequest(
query,
@@ -64,7 +67,10 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
/// 即将执行的 legacy 查询对象。
/// 命中时返回可用于 CQRS runtime 的架构上下文。
/// 如果既接入了 runtime 且查询对象提供了上下文,则返回 。
- private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ [MemberNotNullWhen(true, nameof(_runtime))]
+ private bool TryResolveDispatchContext(
+ object query,
+ out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
context = null!;
diff --git a/GFramework.Core/Query/QueryExecutor.cs b/GFramework.Core/Query/QueryExecutor.cs
index c8f2fdd2..2ecf0960 100644
--- a/GFramework.Core/Query/QueryExecutor.cs
+++ b/GFramework.Core/Query/QueryExecutor.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
+using System.Diagnostics.CodeAnalysis;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
@@ -35,7 +36,7 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
{
ArgumentNullException.ThrowIfNull(query);
- if (TryResolveDispatchContext(query, out var context) && _runtime is not null)
+ if (TryResolveDispatchContext(query, out var context))
{
var boxedResult = _runtime.SendAsync(
context,
@@ -57,7 +58,10 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
/// 即将执行的 legacy 查询对象。
/// 命中时返回可用于 CQRS runtime 的架构上下文。
/// 如果既接入了 runtime 且查询对象提供了上下文,则返回 。
- private bool TryResolveDispatchContext(object query, out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ [MemberNotNullWhen(true, nameof(_runtime))]
+ private bool TryResolveDispatchContext(
+ object query,
+ out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
context = null!;
diff --git a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs
index e1a0524d..2d46354d 100644
--- a/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs
+++ b/GFramework.Core/Services/Modules/AsyncQueryExecutorModule.cs
@@ -33,11 +33,19 @@ public sealed class AsyncQueryExecutorModule : IServiceModule
/// 注册异步查询执行器到依赖注入容器。
/// 创建异步查询执行器实例并将其注册为多例服务。
///
- /// 依赖注入容器实例。
+ /// 承载异步查询执行器与 CQRS runtime 的依赖注入容器实例。
+ /// 为 。
+ ///
+ /// 容器中尚未注册唯一的 实例,无法构建统一 runtime 版本的异步查询执行器。
+ ///
+ ///
+ /// 该模块会在注册阶段立即解析 ,因此
+ /// 必须先于当前模块完成注册。
+ ///
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
- container.RegisterPlurality(new AsyncQueryExecutor(container.Get()));
+ container.RegisterPlurality(new AsyncQueryExecutor(container.GetRequired()));
}
///
diff --git a/GFramework.Core/Services/Modules/CommandExecutorModule.cs b/GFramework.Core/Services/Modules/CommandExecutorModule.cs
index 3d327cda..773cdccd 100644
--- a/GFramework.Core/Services/Modules/CommandExecutorModule.cs
+++ b/GFramework.Core/Services/Modules/CommandExecutorModule.cs
@@ -33,11 +33,19 @@ public sealed class CommandExecutorModule : IServiceModule
/// 注册命令执行器到依赖注入容器。
/// 创建命令执行器实例并将其注册为多例服务。
///
- /// 依赖注入容器实例。
+ /// 承载命令执行器与 CQRS runtime 的依赖注入容器实例。
+ /// 为 。
+ ///
+ /// 容器中尚未注册唯一的 实例,无法构建统一 runtime 版本的命令执行器。
+ ///
+ ///
+ /// 该模块会在注册阶段立即解析 ,因此
+ /// 必须先于当前模块完成注册。
+ ///
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
- container.RegisterPlurality(new CommandExecutor(container.Get()));
+ container.RegisterPlurality(new CommandExecutor(container.GetRequired()));
}
///
diff --git a/GFramework.Core/Services/Modules/QueryExecutorModule.cs b/GFramework.Core/Services/Modules/QueryExecutorModule.cs
index 40e1eb91..41edbe22 100644
--- a/GFramework.Core/Services/Modules/QueryExecutorModule.cs
+++ b/GFramework.Core/Services/Modules/QueryExecutorModule.cs
@@ -33,11 +33,19 @@ public sealed class QueryExecutorModule : IServiceModule
/// 注册查询执行器到依赖注入容器。
/// 创建查询执行器实例并将其注册为多例服务。
///
- /// 依赖注入容器实例。
+ /// 承载查询执行器与 CQRS runtime 的依赖注入容器实例。
+ /// 为 。
+ ///
+ /// 容器中尚未注册唯一的 实例,无法构建统一 runtime 版本的查询执行器。
+ ///
+ ///
+ /// 该模块会在注册阶段立即解析 ,因此
+ /// 必须先于当前模块完成注册。
+ ///
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
- container.RegisterPlurality(new QueryExecutor(container.Get()));
+ container.RegisterPlurality(new QueryExecutor(container.GetRequired()));
}
///
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index 0d6ed018..ab97559e 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,9 +7,9 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-093`
+- 恢复点编号:`CQRS-REWRITE-RP-094`
- 当前阶段:`Phase 8`
-- 当前 PR 锚点:`待创建(当前分支 feat/cqrs-optimization 尚未为 RP-092 建立新 PR)`
+- 当前 PR 锚点:`PR #334`
- 当前结论:
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归
@@ -28,8 +28,9 @@ CQRS 迁移与收敛。
- 当前 `RP-090` 已收敛 `PR #326` benchmark review:统一 benchmark 最小宿主构建、冻结 GFramework 容器、限制 MediatR 扫描范围,并恢复 request startup cold-start 对照
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
- `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
- - 当前 `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
-- `ai-plan` active 入口现以 `RP-093` 为最新恢复锚点;`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
+ - 当前 `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
+- `ai-plan` active 入口现以 `RP-094` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
@@ -41,6 +42,9 @@ CQRS 迁移与收敛。
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime`
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
- `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
+- `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册
+- `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退
+- `LegacyAsyncQueryDispatchRequestHandler` 与 `LegacyAsyncCommandResultDispatchRequestHandler` 现通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
@@ -51,6 +55,7 @@ CQRS 迁移与收敛。
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
+- `PR #334` 当前 latest-head open AI feedback 已集中到 legacy bridge / 文档收尾;本轮本地修复后,剩余 thread 应主要是待 GitHub 重新索引的状态差异或低价值建议
## 当前风险
@@ -61,6 +66,7 @@ CQRS 迁移与收敛。
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
- legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline;若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成”
+- `LegacyBridgePipelineTracker` 仍是进程级静态测试辅助;虽然现在已在相关 fixture 清理阶段重置并补充线程安全说明,但若将来扩大并行 bridge fixture 数量,仍要继续控制共享状态扩散
## 最近权威验证
@@ -108,12 +114,25 @@ CQRS 迁移与收敛。
- 结果:通过
- `git diff --check`
- 结果:通过
+- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
+ - 结果:通过
+ - 备注:确认当前分支对应 `PR #334`;仍有效的 latest-head review 已收敛到 legacy bridge 测试装配、运行时依赖契约、异步取消、XML 文档与兼容文档边界
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:修复新增 XML 文档 warning 后复跑,当前 `GFramework.Core` 三个 target framework 均已干净通过
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
+ - 结果:通过,`48/48` passed
+ - 备注:覆盖 legacy bridge 兼容入口、测试装配、执行器 runtime fallback 与相关模块行为
+- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
## 下一推荐步骤
-1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进
-2. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责
-3. 若回到 benchmark 方向,优先补 `stream handler` 生命周期矩阵;若要扩到 `Scoped` 生命周期,先为 benchmark 宿主设计真实显式 scope 基线
+1. 先用新提交和最新 CI 再跑一次 `$gframework-pr-review`,确认 `PR #334` 的 latest-head open threads 是否已实质清空
+2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进
+3. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责
## 活跃文档
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 76264060..25255efc 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,38 @@
## 2026-05-07
+### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094)
+
+- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`
+- latest-head open AI review 复核后,主线程接受并执行的修复集中在六类:
+ - `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` 通过字符串字面量反射实例化内部 bridge handler,维护成本高且不利于 rename-safe 重构
+ - `ArchitectureModulesBehaviorTests` 在断言失败路径下未保证 `DestroyAsync()` 执行,且 `TearDown` 未重置 `LegacyBridgePipelineTracker`
+ - `LegacyBridgePipelineTracker` 以静态共享计数器记录 bridge pipeline 命中,但未文档化线程安全语义,且用字符串匹配类型名识别 bridge request
+ - `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 丢弃了 runtime 传入的 `CancellationToken`
+ - `CommandExecutorModule` / `QueryExecutorModule` / `AsyncQueryExecutorModule` 依赖 `container.Get()` 的隐式注册顺序,但此前既未显式失败,也未写进 API 契约
+ - 多个 legacy bridge request / docs 页面仍缺 XML 文档或回退边界说明
+- 本轮主线程决策:
+ - 为 `GFramework.Core` 新增 `Properties/AssemblyInfo.cs`,用 `InternalsVisibleTo("GFramework.Core.Tests")` 让测试直接实例化内部 handler
+ - 把 `ArchitectureContextTests.RegisterLegacyBridgeHandlers` 改成显式构造 6 个 handler,移除字符串反射装配
+ - 为 bridge 相关测试补 `TearDown` 清理和 `try/finally` 销毁,减少失败路径资源泄露
+ - 为 `LegacyBridgePipelineTracker` 增补 ``,并改用 `typeof(LegacyCqrsDispatchRequestBase).IsAssignableFrom(requestType)` 识别 bridge request
+ - 为 `LegacyAsyncQueryDispatchRequestHandler` / `LegacyAsyncCommandResultDispatchRequestHandler` 加入预取消检查与 `WaitAsync(cancellationToken)`
+ - 将三个 executor module 改为 `GetRequired()`,同时在 XML 文档中显式声明 `CqrsRuntimeModule` 的前置注册约束
+ - 为 `CommandExecutor` / `QueryExecutor` / `AsyncQueryExecutor` 的 dispatch-context helper 增加 `[MemberNotNullWhen]`,收敛重复 `_runtime is not null` 判空与 null-forgiving
+ - 补齐 legacy bridge request / handler 的 XML 文档,以及 `docs/zh-CN/core/command.md`、`context.md` 的 fallback 边界说明
+- 本轮没有跟进的 thread:
+ - `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs` 的 `sealed` 建议属于低价值性能/风格提示,不影响 `PR #334` 的行为正确性
+ - 若 review 在 GitHub 重新索引前仍显示旧 thread,下一轮以最新 head commit 再次抓取为准,不在本地重复造改动
+- 本轮权威验证:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
+ - 结果:通过,`48/48` passed
+ - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+
### 阶段:legacy Core CQRS -> GFramework.Cqrs bridge(CQRS-REWRITE-RP-093)
- 延续 `$gframework-batch-boot 50`,本轮明确不只盯 benchmark,而是同时处理两个目标:
diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md
index 49e74611..25355a72 100644
--- a/docs/zh-CN/core/command.md
+++ b/docs/zh-CN/core/command.md
@@ -107,6 +107,7 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
在标准架构启动路径中,这些兼容入口底层已经统一改走 `ICqrsRuntime`。
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
+只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline,也不会额外补上下文桥接链路。
在 `IContextAware` 对象内,通常直接通过扩展使用:
diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md
index 128e27ee..5b604e6c 100644
--- a/docs/zh-CN/core/context.md
+++ b/docs/zh-CN/core/context.md
@@ -113,7 +113,7 @@ this.SendEvent(new PlayerDiedEvent());
在标准 `Architecture` 初始化路径里,这些旧入口现在会复用同一个 `ICqrsRuntime`:
旧 `SendCommand(...)` / `SendQuery(...)` 仍保持原有调用方式,但会经过统一的 request pipeline 与上下文注入链路。
-只有在你直接 `new CommandExecutor()`、`new QueryExecutor()` 做隔离测试,且没有提供 runtime 时,才会回退到 legacy 直接执行。
+只有在你直接 `new CommandExecutor()`、`new QueryExecutor()` 或 `new AsyncQueryExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时 `SendQueryAsync(...)` 兼容入口也会沿用同样的 legacy 路径,而不会进入统一 runtime pipeline。
## 新 CQRS 入口
From dc3bd3744e2ceaa557ef03bc991fc88daedb460b Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 19:00:49 +0800
Subject: [PATCH 4/7] =?UTF-8?q?fix(core):=20=E6=94=B6=E5=8F=A3=20legacy=20?=
=?UTF-8?q?bridge=20=E5=90=8C=E6=AD=A5=E8=AF=84=E5=AE=A1=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 legacy 同步 bridge 的 runtime 等待方式,统一通过共享 helper 隔离同步上下文并收口重复 dispatch-context 解析逻辑
- 补充 legacy async command bridge 的取消可见性,并更新 ICqrsRuntime 与相关入口的契约说明
- 新增 bridge 回归测试并更新 cqrs-rewrite active tracking,覆盖同步上下文隔离、测试容器释放与取消语义
---
.../Architectures/ArchitectureContextTests.cs | 74 ++++++++++-----
.../ArchitectureModulesBehaviorTests.cs | 1 +
.../Command/CommandExecutorTests.cs | 61 ++++++++++++
.../Command/ContextAwareLegacyCommand.cs | 31 ++++++
.../ContextAwareLegacyCommandWithResult.cs | 26 +++++
.../Command/RecordingCqrsRuntime.cs | 66 +++++++++++++
.../TestLegacySynchronizationContext.cs | 11 +++
...AsyncCommandDispatchRequestHandlerTests.cs | 87 +++++++++++++++++
.../Query/AsyncQueryExecutorTests.cs | 30 ++++++
.../Query/ContextAwareLegacyAsyncQuery.cs | 26 +++++
.../Query/ContextAwareLegacyQuery.cs | 26 +++++
.../Query/QueryExecutorTests.cs | 42 +++++++++
.../Architectures/ArchitectureContext.cs | 9 +-
GFramework.Core/Command/CommandExecutor.cs | 55 ++++-------
...egacyAsyncCommandDispatchRequestHandler.cs | 4 +-
.../Cqrs/LegacyCqrsDispatchHelper.cs | 94 +++++++++++++++++++
GFramework.Core/Query/AsyncQueryExecutor.cs | 36 +------
GFramework.Core/Query/QueryExecutor.cs | 57 ++++-------
.../Cqrs/ICqrsRuntime.cs | 3 +
.../todos/cqrs-rewrite-migration-tracking.md | 22 ++++-
.../traces/cqrs-rewrite-migration-trace.md | 22 +++++
21 files changed, 640 insertions(+), 143 deletions(-)
create mode 100644 GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
create mode 100644 GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs
create mode 100644 GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
create mode 100644 GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs
create mode 100644 GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs
create mode 100644 GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
create mode 100644 GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
create mode 100644 GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
index 835930d0..1b8ec488 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
@@ -43,6 +43,7 @@ namespace GFramework.Core.Tests.Architectures;
/// - GetUtility方法 - 获取未注册工具时抛出异常
/// - GetEnvironment方法 - 获取环境对象
///
+[NonParallelizable]
[TestFixture]
public class ArchitectureContextTests
{
@@ -150,13 +151,20 @@ public class ArchitectureContextTests
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeQuery();
- var bridgeContext = CreateFrozenBridgeContext();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
- var result = bridgeContext.SendQuery(testQuery);
+ try
+ {
+ var result = bridgeContext.SendQuery(testQuery);
- Assert.That(result, Is.EqualTo(24));
- Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
- Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ Assert.That(result, Is.EqualTo(24));
+ Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
}
///
@@ -190,13 +198,20 @@ public class ArchitectureContextTests
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommand();
- var bridgeContext = CreateFrozenBridgeContext();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
- bridgeContext.SendCommand(testCommand);
+ try
+ {
+ bridgeContext.SendCommand(testCommand);
- Assert.That(testCommand.Executed, Is.True);
- Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
- Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ Assert.That(testCommand.Executed, Is.True);
+ Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
}
///
@@ -230,13 +245,20 @@ public class ArchitectureContextTests
{
LegacyBridgePipelineTracker.Reset();
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
- var bridgeContext = CreateFrozenBridgeContext();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
- var result = bridgeContext.SendCommand(testCommand);
+ try
+ {
+ var result = bridgeContext.SendCommand(testCommand);
- Assert.That(result, Is.EqualTo(42));
- Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
- Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ Assert.That(result, Is.EqualTo(42));
+ Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
}
///
@@ -247,22 +269,30 @@ public class ArchitectureContextTests
{
LegacyBridgePipelineTracker.Reset();
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
- var bridgeContext = CreateFrozenBridgeContext();
+ var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
- var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
+ try
+ {
+ var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
- Assert.That(result, Is.EqualTo(64));
- Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
- Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ Assert.That(result, Is.EqualTo(64));
+ Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
+ Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
+ }
+ finally
+ {
+ bridgeContainer.Dispose();
+ }
}
///
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
///
+ /// 返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。
/// 能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。
- private static ArchitectureContext CreateFrozenBridgeContext()
+ private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container)
{
- var container = new MicrosoftDiContainer();
+ container = new MicrosoftDiContainer();
RegisterLegacyBridgeHandlers(container);
new CqrsRuntimeModule().Register(container);
container.ExecuteServicesHook(services =>
diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
index 4c0b86f8..f803bb56 100644
--- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
+++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs
@@ -15,6 +15,7 @@ namespace GFramework.Core.Tests.Architectures;
/// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 CQRS 行为注册能力。
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
///
+[NonParallelizable]
[TestFixture]
public class ArchitectureModulesBehaviorTests
{
diff --git a/GFramework.Core.Tests/Command/CommandExecutorTests.cs b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
index 869b7384..21c6bc27 100644
--- a/GFramework.Core.Tests/Command/CommandExecutorTests.cs
+++ b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Command;
+using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Command;
@@ -75,6 +76,59 @@ public class CommandExecutorTests
Assert.Throws(() => _commandExecutor.Send(null!));
}
+ ///
+ /// 验证 legacy 同步命令桥接会在线程池上等待 runtime,
+ /// 避免直接继承调用方当前的同步上下文。
+ ///
+ [Test]
+ public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
+ {
+ var runtime = new RecordingCqrsRuntime();
+ var executor = new CommandExecutor(runtime);
+ var command = new ContextAwareLegacyCommand();
+ var expectedContext = new TestArchitectureContextBaseStub();
+ ((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
+ var originalContext = SynchronizationContext.Current;
+
+ try
+ {
+ SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
+
+ executor.Send(command);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(runtime.LastRequest, Is.TypeOf());
+ Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
+ });
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ ///
+ /// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。
+ ///
+ [Test]
+ public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context()
+ {
+ var runtime = new RecordingCqrsRuntime(static _ => 123);
+ var executor = new CommandExecutor(runtime);
+ var command = new ContextAwareLegacyCommandWithResult(123);
+ var expectedContext = new TestArchitectureContextBaseStub();
+ ((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
+
+ var result = executor.Send(command);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Is.EqualTo(123));
+ Assert.That(runtime.LastRequest, Is.TypeOf());
+ });
+ }
+
///
/// 测试SendAsync方法执行异步命令
///
@@ -122,4 +176,11 @@ public class CommandExecutorTests
{
Assert.ThrowsAsync(() => _commandExecutor.SendAsync(null!));
}
+
+ ///
+ /// 为同步 bridge 测试提供最小架构上下文替身。
+ ///
+ private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
+ {
+ }
}
diff --git a/GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs b/GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
new file mode 100644
index 00000000..ffd47b4f
--- /dev/null
+++ b/GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
@@ -0,0 +1,31 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Command;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Command;
+
+///
+/// 为 提供可观察上下文注入的 legacy 命令。
+///
+internal sealed class ContextAwareLegacyCommand : ContextAwareBase, ICommand
+{
+ ///
+ /// 获取执行期间观察到的架构上下文。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ /// 获取命令是否已经执行。
+ ///
+ public bool Executed { get; private set; }
+
+ ///
+ public void Execute()
+ {
+ Executed = true;
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ }
+}
diff --git a/GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs b/GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs
new file mode 100644
index 00000000..21712f73
--- /dev/null
+++ b/GFramework.Core.Tests/Command/ContextAwareLegacyCommandWithResult.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Command;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Command;
+
+///
+/// 为 提供可观察上下文注入的带返回值 legacy 命令。
+///
+internal sealed class ContextAwareLegacyCommandWithResult(int result) : ContextAwareBase, ICommand
+{
+ ///
+ /// 获取执行期间观察到的架构上下文。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ public int Execute()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return result;
+ }
+}
diff --git a/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
new file mode 100644
index 00000000..d85df9a0
--- /dev/null
+++ b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Cqrs.Abstractions.Cqrs;
+
+namespace GFramework.Core.Tests.Command;
+
+///
+/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。
+///
+internal sealed class RecordingCqrsRuntime(Func? responseFactory = null) : ICqrsRuntime
+{
+ private static readonly Func DefaultResponseFactory = _ => null;
+
+ private readonly Func _responseFactory = responseFactory ?? DefaultResponseFactory;
+
+ ///
+ /// 获取最近一次 观察到的同步上下文类型。
+ ///
+ public Type? ObservedSynchronizationContextType { get; private set; }
+
+ ///
+ /// 获取最近一次收到的请求实例。
+ ///
+ public object? LastRequest { get; private set; }
+
+ ///
+ public ValueTask SendAsync(
+ ICqrsContext context,
+ IRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(request);
+
+ ObservedSynchronizationContextType = SynchronizationContext.Current?.GetType();
+ LastRequest = request;
+
+ object? response = request switch
+ {
+ IRequest => Unit.Value,
+ _ => _responseFactory(request)
+ };
+
+ return ValueTask.FromResult((TResponse)response!);
+ }
+
+ ///
+ public ValueTask PublishAsync(
+ ICqrsContext context,
+ TNotification notification,
+ CancellationToken cancellationToken = default)
+ where TNotification : INotification
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ public IAsyncEnumerable CreateStream(
+ ICqrsContext context,
+ IStreamRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs b/GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs
new file mode 100644
index 00000000..21f3aba6
--- /dev/null
+++ b/GFramework.Core.Tests/Command/TestLegacySynchronizationContext.cs
@@ -0,0 +1,11 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+namespace GFramework.Core.Tests.Command;
+
+///
+/// 为 legacy 同步 bridge 回归测试提供可识别的同步上下文占位类型。
+///
+internal sealed class TestLegacySynchronizationContext : SynchronizationContext
+{
+}
diff --git a/GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs b/GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs
new file mode 100644
index 00000000..4d648ef1
--- /dev/null
+++ b/GFramework.Core.Tests/Cqrs/LegacyAsyncCommandDispatchRequestHandlerTests.cs
@@ -0,0 +1,87 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Command;
+using GFramework.Core.Abstractions.Rule;
+using GFramework.Core.Cqrs;
+using GFramework.Core.Rule;
+using GFramework.Core.Tests.Architectures;
+
+namespace GFramework.Core.Tests.Cqrs;
+
+///
+/// 验证 legacy 异步无返回值命令 bridge handler 的取消语义。
+///
+[TestFixture]
+public class LegacyAsyncCommandDispatchRequestHandlerTests
+{
+ ///
+ /// 验证当取消令牌在执行前已触发时,handler 不会启动底层 legacy 命令。
+ ///
+ [Test]
+ public void Handle_Should_Throw_Without_Executing_Command_When_Cancellation_Is_Already_Requested()
+ {
+ var handler = new LegacyAsyncCommandDispatchRequestHandler();
+ var command = new ProbeAsyncCommand(Task.CompletedTask);
+ using var cancellationTokenSource = new CancellationTokenSource();
+ cancellationTokenSource.Cancel();
+
+ Assert.ThrowsAsync(
+ async () => await handler.Handle(
+ new LegacyAsyncCommandDispatchRequest(command),
+ cancellationTokenSource.Token)
+ .AsTask()
+ .ConfigureAwait(false));
+ Assert.That(command.ExecutionCount, Is.Zero);
+ }
+
+ ///
+ /// 验证当底层 legacy 命令正在运行时,handler 会通过 WaitAsync 及时向调用方暴露取消。
+ ///
+ [Test]
+ public async Task Handle_Should_Observe_Cancellation_While_Command_Is_Running()
+ {
+ var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var handler = new LegacyAsyncCommandDispatchRequestHandler();
+ var command = new ProbeAsyncCommand(completionSource.Task);
+ using var cancellationTokenSource = new CancellationTokenSource();
+ ((IContextAware)handler).SetContext(new TestArchitectureContextBaseStub());
+
+ var handleTask = handler.Handle(
+ new LegacyAsyncCommandDispatchRequest(command),
+ cancellationTokenSource.Token)
+ .AsTask();
+
+ cancellationTokenSource.Cancel();
+
+ Assert.That(
+ async () => await handleTask.ConfigureAwait(false),
+ Throws.InstanceOf());
+ Assert.That(command.ExecutionCount, Is.EqualTo(1));
+ }
+
+ ///
+ /// 为 handler 取消测试提供可控完成时机的异步命令替身。
+ ///
+ private sealed class ProbeAsyncCommand(Task executionTask) : ContextAwareBase, IAsyncCommand
+ {
+ ///
+ /// 获取底层命令逻辑的触发次数。
+ ///
+ public int ExecutionCount { get; private set; }
+
+ ///
+ public Task ExecuteAsync()
+ {
+ ExecutionCount++;
+ return executionTask;
+ }
+ }
+
+ ///
+ /// 为 handler 取消测试提供最小架构上下文替身。
+ ///
+ private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
+ {
+ }
+}
diff --git a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs
index 434c8600..8ac9f002 100644
--- a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs
+++ b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Query;
+using GFramework.Core.Tests.Architectures;
+using GFramework.Core.Tests.Command;
namespace GFramework.Core.Tests.Query;
@@ -138,4 +140,32 @@ public class AsyncQueryExecutorTests
Assert.That(result1, Is.EqualTo(20));
Assert.That(result2, Is.EqualTo(40));
}
+
+ ///
+ /// 验证 legacy 异步查询桥接会保留上下文注入,并通过 runtime 返回结果。
+ ///
+ [Test]
+ public async Task SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context()
+ {
+ var runtime = new RecordingCqrsRuntime(static _ => 64);
+ var executor = new AsyncQueryExecutor(runtime);
+ var query = new ContextAwareLegacyAsyncQuery(64);
+ var expectedContext = new TestArchitectureContextBaseStub();
+ ((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
+
+ var result = await executor.SendAsync(query);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Is.EqualTo(64));
+ Assert.That(runtime.LastRequest, Is.TypeOf());
+ });
+ }
+
+ ///
+ /// 为异步 bridge 测试提供最小架构上下文替身。
+ ///
+ private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
+ {
+ }
}
diff --git a/GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs b/GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
new file mode 100644
index 00000000..77b6ef02
--- /dev/null
+++ b/GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Query;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Query;
+
+///
+/// 为 提供可观察上下文注入的 legacy 异步查询。
+///
+internal sealed class ContextAwareLegacyAsyncQuery(int result) : ContextAwareBase, IAsyncQuery
+{
+ ///
+ /// 获取执行期间观察到的架构上下文。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ public Task DoAsync()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return Task.FromResult(result);
+ }
+}
diff --git a/GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs b/GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
new file mode 100644
index 00000000..ad50babe
--- /dev/null
+++ b/GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Query;
+using GFramework.Core.Rule;
+
+namespace GFramework.Core.Tests.Query;
+
+///
+/// 为 提供可观察上下文注入的 legacy 查询。
+///
+internal sealed class ContextAwareLegacyQuery(int result) : ContextAwareBase, IQuery
+{
+ ///
+ /// 获取执行期间观察到的架构上下文。
+ ///
+ public IArchitectureContext? ObservedContext { get; private set; }
+
+ ///
+ public int Do()
+ {
+ ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
+ return result;
+ }
+}
diff --git a/GFramework.Core.Tests/Query/QueryExecutorTests.cs b/GFramework.Core.Tests/Query/QueryExecutorTests.cs
index fc2415cc..e5adaf79 100644
--- a/GFramework.Core.Tests/Query/QueryExecutorTests.cs
+++ b/GFramework.Core.Tests/Query/QueryExecutorTests.cs
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Query;
+using GFramework.Core.Tests.Architectures;
+using GFramework.Core.Tests.Command;
namespace GFramework.Core.Tests.Query;
@@ -61,4 +63,44 @@ public class QueryExecutorTests
Assert.That(result, Is.EqualTo("Result: 10"));
}
+
+ ///
+ /// 验证 legacy 同步查询桥接会在线程池上等待 runtime,
+ /// 避免直接复用调用方的同步上下文。
+ ///
+ [Test]
+ public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
+ {
+ var runtime = new RecordingCqrsRuntime(static _ => 24);
+ var executor = new QueryExecutor(runtime);
+ var query = new ContextAwareLegacyQuery(24);
+ var expectedContext = new TestArchitectureContextBaseStub();
+ ((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
+ var originalContext = SynchronizationContext.Current;
+
+ try
+ {
+ SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
+
+ var result = executor.Send(query);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Is.EqualTo(24));
+ Assert.That(runtime.LastRequest, Is.TypeOf());
+ Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
+ });
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(originalContext);
+ }
+ }
+
+ ///
+ /// 为同步 bridge 测试提供最小架构上下文替身。
+ ///
+ private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
+ {
+ }
}
diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs
index e143c957..f314a0b8 100644
--- a/GFramework.Core/Architectures/ArchitectureContext.cs
+++ b/GFramework.Core/Architectures/ArchitectureContext.cs
@@ -112,7 +112,8 @@ public class ArchitectureContext : IArchitectureContext
/// 响应结果
public TResponse SendRequest(IRequest request)
{
- return SendRequestAsync(request).AsTask().GetAwaiter().GetResult();
+ ArgumentNullException.ThrowIfNull(request);
+ return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request);
}
///
@@ -197,7 +198,8 @@ public class ArchitectureContext : IArchitectureContext
/// 查询结果
public TResponse SendQuery(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query)
{
- return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
+ ArgumentNullException.ThrowIfNull(query);
+ return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, query);
}
///
@@ -403,7 +405,8 @@ public class ArchitectureContext : IArchitectureContext
/// 命令执行结果
public TResponse SendCommand(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command)
{
- return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
+ ArgumentNullException.ThrowIfNull(command);
+ return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, command);
}
///
diff --git a/GFramework.Core/Command/CommandExecutor.cs b/GFramework.Core/Command/CommandExecutor.cs
index fd401c9d..83d6ad80 100644
--- a/GFramework.Core/Command/CommandExecutor.cs
+++ b/GFramework.Core/Command/CommandExecutor.cs
@@ -1,9 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
-using System.Diagnostics.CodeAnalysis;
using GFramework.Core.Abstractions.Command;
-using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
@@ -78,9 +76,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
{
ArgumentNullException.ThrowIfNull(command);
- if (TryResolveDispatchContext(command, out var context))
+ var cqrsRuntime = _runtime;
+
+ if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
{
- return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
+ return cqrsRuntime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
}
return command.ExecuteAsync();
@@ -97,9 +97,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
{
ArgumentNullException.ThrowIfNull(command);
- if (TryResolveDispatchContext(command, out var context))
+ var cqrsRuntime = _runtime;
+
+ if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
{
- return BridgeAsyncCommandWithResultAsync(_runtime, context, command);
+ return BridgeAsyncCommandWithResultAsync(cqrsRuntime, context, command);
}
return command.ExecuteAsync();
@@ -119,12 +121,14 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
where TTarget : class
where TRequest : IRequest
{
- if (!TryResolveDispatchContext(target, out var context))
+ var cqrsRuntime = _runtime;
+
+ if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
{
return false;
}
- _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
+ LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
return true;
}
@@ -145,13 +149,15 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
where TTarget : class
where TRequest : IRequest
{
- if (!TryResolveDispatchContext(target, out var context))
+ var cqrsRuntime = _runtime;
+
+ if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
{
result = default;
return false;
}
- var boxedResult = _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
+ var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
result = (TResult)boxedResult!;
return true;
}
@@ -177,33 +183,4 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
.ConfigureAwait(false);
return (TResult)boxedResult!;
}
-
- ///
- /// 解析当前 legacy 目标对象应该绑定到哪个架构上下文。
- ///
- /// 即将执行的 legacy 目标对象。
- /// 命中时返回可用于 CQRS runtime 的架构上下文。
- /// 如果既接入了 runtime 且目标对象提供了上下文,则返回 。
- [MemberNotNullWhen(true, nameof(_runtime))]
- private bool TryResolveDispatchContext(
- object target,
- out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
- {
- context = null!;
-
- if (_runtime is null || target is not IContextAware contextAware)
- {
- return false;
- }
-
- try
- {
- context = contextAware.GetContext();
- return true;
- }
- catch (InvalidOperationException)
- {
- return false;
- }
- }
}
diff --git a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs
index e34e9de3..4e0b56ca 100644
--- a/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs
+++ b/GFramework.Core/Cqrs/LegacyAsyncCommandDispatchRequestHandler.cs
@@ -17,8 +17,10 @@ internal sealed class LegacyAsyncCommandDispatchRequestHandler
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
+ // Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can still observe cancellation promptly.
+ cancellationToken.ThrowIfCancellationRequested();
PrepareTarget(request.Command);
- await request.Command.ExecuteAsync().ConfigureAwait(false);
+ await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}
diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
new file mode 100644
index 00000000..5fb6fec0
--- /dev/null
+++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
@@ -0,0 +1,94 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Rule;
+using GFramework.Cqrs.Abstractions.Cqrs;
+
+namespace GFramework.Core.Cqrs;
+
+///
+/// 为 legacy Core CQRS bridge 提供共享的上下文解析与同步兼容辅助逻辑。
+///
+///
+/// 旧的同步 Command/Query 入口仍需要阻塞等待统一 返回结果。
+/// 这里统一通过 把等待动作切换到线程池,
+/// 避免直接占用调用方的 导致 legacy 同步入口与异步 pipeline 互相卡死。
+///
+internal static class LegacyCqrsDispatchHelper
+{
+ ///
+ /// 解析当前 legacy 目标对象是否能够绑定到统一 CQRS runtime 的架构上下文。
+ ///
+ /// 当前执行器可用的统一 CQRS runtime。
+ /// 即将执行的 legacy 目标对象。
+ /// 命中时返回可用于 CQRS runtime 的架构上下文。
+ ///
+ /// 当 可用且 能稳定提供
+ /// 时返回 ;否则返回 。
+ ///
+ internal static bool TryResolveDispatchContext(
+ [NotNullWhen(true)] ICqrsRuntime? runtime,
+ object target,
+ out IArchitectureContext context)
+ {
+ ArgumentNullException.ThrowIfNull(target);
+
+ context = null!;
+
+ if (runtime is null || target is not IContextAware contextAware)
+ {
+ return false;
+ }
+
+ try
+ {
+ context = contextAware.GetContext();
+ return true;
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 同步等待统一 CQRS runtime 完成无返回值请求。
+ ///
+ /// 负责分发当前请求的统一 CQRS runtime。
+ /// 当前架构上下文。
+ /// 要同步等待的请求。
+ internal static void SendSynchronously(
+ ICqrsRuntime runtime,
+ IArchitectureContext context,
+ IRequest request)
+ {
+ ArgumentNullException.ThrowIfNull(runtime);
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(request);
+
+ Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// 同步等待统一 CQRS runtime 完成带返回值请求,并返回实际响应。
+ ///
+ /// 请求响应类型。
+ /// 负责分发当前请求的统一 CQRS runtime。
+ /// 当前架构上下文。
+ /// 要同步等待的请求。
+ /// 统一 CQRS runtime 返回的响应结果。
+ internal static TResponse SendSynchronously(
+ ICqrsRuntime runtime,
+ IArchitectureContext context,
+ IRequest request)
+ {
+ ArgumentNullException.ThrowIfNull(runtime);
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(request);
+
+ return Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
+ }
+}
diff --git a/GFramework.Core/Query/AsyncQueryExecutor.cs b/GFramework.Core/Query/AsyncQueryExecutor.cs
index f59b63f9..4890d9d0 100644
--- a/GFramework.Core/Query/AsyncQueryExecutor.cs
+++ b/GFramework.Core/Query/AsyncQueryExecutor.cs
@@ -1,9 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
-using System.Diagnostics.CodeAnalysis;
using GFramework.Core.Abstractions.Query;
-using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
@@ -31,9 +29,11 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
{
ArgumentNullException.ThrowIfNull(query);
- if (TryResolveDispatchContext(query, out var context))
+ var cqrsRuntime = _runtime;
+
+ if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
{
- return BridgeAsyncQueryAsync(_runtime, context, query);
+ return BridgeAsyncQueryAsync(cqrsRuntime, context, query);
}
return query.DoAsync();
@@ -61,32 +61,4 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
return (TResult)boxedResult!;
}
- ///
- /// 解析当前 legacy 查询应该绑定到哪个架构上下文。
- ///
- /// 即将执行的 legacy 查询对象。
- /// 命中时返回可用于 CQRS runtime 的架构上下文。
- /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 。
- [MemberNotNullWhen(true, nameof(_runtime))]
- private bool TryResolveDispatchContext(
- object query,
- out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
- {
- context = null!;
-
- if (_runtime is null || query is not IContextAware contextAware)
- {
- return false;
- }
-
- try
- {
- context = contextAware.GetContext();
- return true;
- }
- catch (InvalidOperationException)
- {
- return false;
- }
- }
}
diff --git a/GFramework.Core/Query/QueryExecutor.cs b/GFramework.Core/Query/QueryExecutor.cs
index 2ecf0960..516644bc 100644
--- a/GFramework.Core/Query/QueryExecutor.cs
+++ b/GFramework.Core/Query/QueryExecutor.cs
@@ -1,9 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
-using System.Diagnostics.CodeAnalysis;
using GFramework.Core.Abstractions.Query;
-using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
@@ -31,53 +29,30 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
///
/// 查询结果的类型。
/// 要执行的查询对象,必须实现 IQuery<TResult> 接口。
- /// 查询执行的结果,类型为 TResult。
+ /// 查询执行成功后还原出的 结果。
+ ///
+ /// 统一 CQRS runtime 返回 ,但 为值类型。
+ ///
+ ///
+ /// 统一 CQRS runtime 返回的装箱结果无法转换为 。
+ ///
public TResult Send(IQuery query)
{
ArgumentNullException.ThrowIfNull(query);
- if (TryResolveDispatchContext(query, out var context))
+ var cqrsRuntime = _runtime;
+
+ if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
{
- var boxedResult = _runtime.SendAsync(
- context,
- new LegacyQueryDispatchRequest(
- query,
- () => query.Do()))
- .AsTask()
- .GetAwaiter()
- .GetResult();
+ var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(
+ cqrsRuntime,
+ context,
+ new LegacyQueryDispatchRequest(
+ query,
+ () => query.Do()));
return (TResult)boxedResult!;
}
return query.Do();
}
-
- ///
- /// 解析当前 legacy 查询应该绑定到哪个架构上下文。
- ///
- /// 即将执行的 legacy 查询对象。
- /// 命中时返回可用于 CQRS runtime 的架构上下文。
- /// 如果既接入了 runtime 且查询对象提供了上下文,则返回 。
- [MemberNotNullWhen(true, nameof(_runtime))]
- private bool TryResolveDispatchContext(
- object query,
- out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
- {
- context = null!;
-
- if (_runtime is null || query is not IContextAware contextAware)
- {
- return false;
- }
-
- try
- {
- context = contextAware.GetContext();
- return true;
- }
- catch (InvalidOperationException)
- {
- return false;
- }
- }
}
diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs
index efb376e6..fe3bfcf2 100644
--- a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs
+++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs
@@ -28,6 +28,9 @@ public interface ICqrsRuntime
///
/// 该契约允许调用方传入任意 ,
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 IArchitectureContext。
+ /// 为了兼容 legacy 同步入口,ArchitectureContext、QueryExecutor 与 CommandExecutor
+ /// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的
+ /// ,并应优先在内部异步链路上使用 ConfigureAwait(false)。
///
ValueTask SendAsync(
ICqrsContext context,
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index ab97559e..1911872f 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-094`
+- 恢复点编号:`CQRS-REWRITE-RP-095`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #334`
- 当前结论:
@@ -29,8 +29,9 @@ CQRS 迁移与收敛。
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
- `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
- `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
- - 当前 `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
-- `ai-plan` active 入口现以 `RP-094` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
+ - 当前 `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
+- `ai-plan` active 入口现以 `RP-095` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
@@ -42,9 +43,13 @@ CQRS 迁移与收敛。
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime`
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
- `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
+- `LegacyCqrsDispatchHelper` 现统一负责 runtime dispatch context 解析,以及 legacy 同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待
+- `ArchitectureContext`、`CommandExecutor`、`QueryExecutor` 的同步 CQRS/legacy bridge 入口不再直接在调用线程上阻塞 `SendAsync(...).GetAwaiter().GetResult()`
- `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册
+- 使用 `LegacyBridgePipelineTracker` 的 `ArchitectureContextTests` 与 `ArchitectureModulesBehaviorTests` 现都显式标记为 `NonParallelizable`
+- `ArchitectureContextTests.CreateFrozenBridgeContext(...)` 现把冻结容器所有权显式交回调用方,并在每个 bridge 用例的 `finally` 中释放
- `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退
-- `LegacyAsyncQueryDispatchRequestHandler` 与 `LegacyAsyncCommandResultDispatchRequestHandler` 现通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性
+- `LegacyAsyncQueryDispatchRequestHandler`、`LegacyAsyncCommandResultDispatchRequestHandler`、`LegacyAsyncCommandDispatchRequestHandler` 现都通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
@@ -55,7 +60,7 @@ CQRS 迁移与收敛。
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
-- `PR #334` 当前 latest-head open AI feedback 已集中到 legacy bridge / 文档收尾;本轮本地修复后,剩余 thread 应主要是待 GitHub 重新索引的状态差异或低价值建议
+- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread
## 当前风险
@@ -127,6 +132,13 @@ CQRS 迁移与收敛。
- 结果:通过
- `git diff --check`
- 结果:通过
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"`
+ - 结果:通过,`54/54` passed
+ - 备注:覆盖 legacy 同步 bridge 的同步上下文隔离、bridge fixture 容器释放,以及 async void command cancellation 可见性
+- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
## 下一推荐步骤
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 25255efc..900e06f3 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,28 @@
## 2026-05-07
+### 阶段:PR #334 legacy bridge sync follow-up(CQRS-REWRITE-RP-095)
+
+- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并只保留本地复核后仍成立的问题:
+ - `QueryExecutor` / `CommandExecutor` 新增的同步 bridge 仍直接阻塞 `ICqrsRuntime.SendAsync(...)`,在调用方存在 `SynchronizationContext` 时容易放大 sync-over-async 死锁面
+ - `QueryExecutor` / `CommandExecutor` / `AsyncQueryExecutor` 各自保留一份相同的 dispatch-context 解析逻辑,仍有漂移风险
+ - `ArchitectureContextTests` 的 bridge fixture 依然共享静态 tracker 且未显式声明非并行;冻结容器所有权也未交还给调用方释放
+ - `LegacyAsyncCommandDispatchRequestHandler` 仍未沿用另两个 async bridge handler 的取消可见性模式
+- 本轮主线程决策:
+ - 新增 `GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs`,统一收口 legacy bridge 的 dispatch-context 解析,以及同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待
+ - 将 `QueryExecutor`、`CommandExecutor`、`AsyncQueryExecutor` 的重复 helper 改为复用共享 helper,并把 `ArchitectureContext` 的同步 CQRS 包装入口一并切换到同一阻塞策略,避免留下半修状态
+ - 为 `ICqrsRuntime.SendAsync(...)` 补充 ``,显式说明 legacy 同步入口会在后台线程上等待该异步契约,处理链路不应依赖调用方 `SynchronizationContext`
+ - 把 `ArchitectureContextTests`、`ArchitectureModulesBehaviorTests` 标记为 `NonParallelizable`,并让 `CreateFrozenBridgeContext(...)` 把冻结容器通过 `out` 参数返还给每个测试在 `finally` 中释放
+ - 为 `LegacyAsyncCommandDispatchRequestHandler` 增补 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)`,与另外两个 async bridge handler 保持一致
+ - 新增回归测试覆盖同步 bridge 的 `SynchronizationContext` 隔离、legacy async command handler 的取消语义,以及 async/sync bridge request 的 request-type 命中
+- 本轮权威验证:
+ - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"`
+ - 结果:通过,`54/54` passed
+
### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094)
- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`
From cca413042fdd6047bfe049805449c2ae86c7d4e8 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 19:52:14 +0800
Subject: [PATCH 5/7] =?UTF-8?q?chore(cqrs-rewrite):=20=E5=90=8C=E6=AD=A5PR?=
=?UTF-8?q?334=E8=AF=84=E5=AE=A1=E5=A4=8D=E6=A0=B8=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 更新 active tracking 与 trace 到 RP-096,记录 latest-head review 的最新权威结论
- 补充 PR #334 当前 stale open thread、CI 测试与 MegaLinter 噪音的本地复核结果
---
.../todos/cqrs-rewrite-migration-tracking.md | 18 ++++++++++++------
.../traces/cqrs-rewrite-migration-trace.md | 17 +++++++++++++++++
2 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index 1911872f..b46e19bb 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-095`
+- 恢复点编号:`CQRS-REWRITE-RP-096`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #334`
- 当前结论:
@@ -30,8 +30,9 @@ CQRS 迁移与收敛。
- `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
- `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
- `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
- - 当前 `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
-- `ai-plan` active 入口现以 `RP-095` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
+ - 当前 `RP-096` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,确认仍显示为 open 的 AI threads 在本地代码中已无新增仍成立的运行时 / 测试 / 文档缺陷,剩余差异主要是 GitHub thread 未 resolve 的状态滞后
+- `ai-plan` active 入口现以 `RP-096` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
@@ -53,12 +54,12 @@ CQRS 迁移与收敛。
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
-- latest-head review 现仍有少量 open thread,但本地复核后,仍成立的问题已收敛到 benchmark 对照公平性、workflow 输入安全性与 active 文档压缩
+- `PR #334` 在 `2026-05-07` 的 latest-head review 仍显示 `CodeRabbit 10` / `Greptile 3` 个 open thread,但本地逐项复核后未发现新的仍成立代码或文档缺陷;当前差异主要来自已实质修复但尚未 resolve 的 thread 状态
- benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型
- `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered`
- `BenchmarkDotNet` 在当前 agent 沙箱里会因自动生成的 bootstrap 脚本异常失败;同一 `dotnet run --no-build` 命令在沙箱外执行通过,因此本轮以沙箱外结果作为 benchmark 权威验证
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
-- 远端 `CTRF` 最新汇总为 `2274/2274` passed
+- 远端 `CTRF` 最新汇总为 `2311/2311` passed(run `#1079`, 2026-05-07)
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread
@@ -91,6 +92,11 @@ CQRS 迁移与收敛。
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:用于验证本轮 request invoker / pipeline / stream invoker 调整与 benchmark workflow 改动后的 Release 编译结果
+- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
+ - 结果:通过
+ - 备注:确认当前分支对应 `PR #334`;`CodeRabbit` latest review 已 `APPROVED`,但 latest-head 仍显示 `10` 个 open thread、`Greptile` 仍显示 `3` 个 open thread;本地逐项复核后未发现新的仍成立缺陷,最新 CI 测试汇总为 `2311/2311` passed,`MegaLinter` 仅剩 `dotnet-format` restore 环境噪音
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output `
- 结果:通过
- 备注:确认当前分支对应 `PR #331`,本轮 latest-head open AI feedback 已收敛到 `dotnet pack --no-build`、共享包校验脚本跨平台兼容性与 active 文档 PR 锚点同步
@@ -142,7 +148,7 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 先用新提交和最新 CI 再跑一次 `$gframework-pr-review`,确认 `PR #334` 的 latest-head open threads 是否已实质清空
+1. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #334` stale review threads,或等待下一次 head 更新后再次用 `$gframework-pr-review` 复核状态是否自动收敛
2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进
3. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 900e06f3..f083ca9b 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,23 @@
## 2026-05-07
+### 阶段:PR #334 latest-head review 复核(CQRS-REWRITE-RP-096)
+
+- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并读取 `/tmp/current-pr-review.json` 中的 `review_agents`、`latest_commit_review`、`megalinter_report` 与 `test_reports`
+- 本轮复核结论:
+ - 当前公开 PR 为 `PR #334`,head commit 为 `dc3bd3744e2ceaa557ef03bc991fc88daedb460b`
+ - `CodeRabbit` latest review 在 `2026-05-07T11:46:42Z` 已是 `APPROVED`,但 latest-head 仍显示 `10` 个 open thread;`Greptile` 仍显示 `3` 个 open thread
+ - 逐条回到本地代码后,相关修复已在当前分支落地:`ArchitectureBootstrapper` 已自动扫描 `typeof(ArchitectureContext).Assembly`;`ArchitectureContextTests` / `ArchitectureModulesBehaviorTests` 已标注 `NonParallelizable` 并保证资源释放;`LegacyAsync*DispatchRequestHandler` 已统一补 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)`;`QueryExecutor` / legacy bridge request XML 文档与 `docs/zh-CN/core/command.md` fallback 说明也已齐备
+ - 远端 CTRF 最新测试汇总为 `2311/2311 passed`(run `#1079`),`MegaLinter` 仅剩 `dotnet-format` restore failed 的环境噪音,没有新的文件级诊断
+- 主线程决策:
+ - 不再为这些 stale open thread 追加新的本地代码改动,避免重复修补已吸收的问题
+ - 仅更新 `cqrs-rewrite` active tracking/trace,把“当前剩余差异主要是 GitHub thread 状态滞后”记录为最新权威事实
+- 本轮权威验证:
+ - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
+ - 结果:通过
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+
### 阶段:PR #334 legacy bridge sync follow-up(CQRS-REWRITE-RP-095)
- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并只保留本地复核后仍成立的问题:
From 44d1a89a0b3e782cd4342873c9b46797d3a7b280 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 20:17:46 +0800
Subject: [PATCH 6/7] =?UTF-8?q?test(core):=20=E8=A1=A5=E5=BC=BA=20legacy?=
=?UTF-8?q?=20bridge=20=E4=B8=8A=E4=B8=8B=E6=96=87=E6=96=AD=E8=A8=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 补充 AsyncQueryExecutor 与 CommandExecutor bridge 测试的上下文保留断言
- 优化 RecordingCqrsRuntime 的 bridge 执行模拟与响应类型诊断
- 更新 cqrs-rewrite active tracking 与 trace 的 RP-097 验证记录
---
.../Command/CommandExecutorTests.cs | 1 +
.../Command/RecordingCqrsRuntime.cs | 145 +++++++++++++++++-
.../Query/AsyncQueryExecutorTests.cs | 1 +
.../todos/cqrs-rewrite-migration-tracking.md | 17 +-
.../traces/cqrs-rewrite-migration-trace.md | 20 +++
5 files changed, 179 insertions(+), 5 deletions(-)
diff --git a/GFramework.Core.Tests/Command/CommandExecutorTests.cs b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
index 21c6bc27..aad98ca3 100644
--- a/GFramework.Core.Tests/Command/CommandExecutorTests.cs
+++ b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
@@ -126,6 +126,7 @@ public class CommandExecutorTests
{
Assert.That(result, Is.EqualTo(123));
Assert.That(runtime.LastRequest, Is.TypeOf());
+ Assert.That(command.ObservedContext, Is.SameAs(expectedContext));
});
}
diff --git a/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
index d85df9a0..5dd07317 100644
--- a/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
+++ b/GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
@@ -1,6 +1,8 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
+using GFramework.Core.Abstractions.Rule;
+using GFramework.Core.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Command;
@@ -25,7 +27,7 @@ internal sealed class RecordingCqrsRuntime(Func? responseFacto
public object? LastRequest { get; private set; }
///
- public ValueTask SendAsync(
+ public async ValueTask SendAsync(
ICqrsContext context,
IRequest request,
CancellationToken cancellationToken = default)
@@ -38,11 +40,34 @@ internal sealed class RecordingCqrsRuntime(Func? responseFacto
object? response = request switch
{
+ LegacyCommandDispatchRequest legacyCommandDispatchRequest => ExecuteLegacyCommand(context, legacyCommandDispatchRequest),
+ LegacyCommandResultDispatchRequest legacyCommandResultDispatchRequest => ExecuteContextAwareRequest(
+ context,
+ legacyCommandResultDispatchRequest.Target,
+ legacyCommandResultDispatchRequest.Execute),
+ LegacyQueryDispatchRequest legacyQueryDispatchRequest => ExecuteContextAwareRequest(
+ context,
+ legacyQueryDispatchRequest.Target,
+ legacyQueryDispatchRequest.Execute),
+ LegacyAsyncCommandDispatchRequest legacyAsyncCommandDispatchRequest => await ExecuteLegacyAsyncCommandAsync(
+ context,
+ legacyAsyncCommandDispatchRequest,
+ cancellationToken).ConfigureAwait(false),
+ LegacyAsyncCommandResultDispatchRequest legacyAsyncCommandResultDispatchRequest => await ExecuteContextAwareRequestAsync(
+ context,
+ legacyAsyncCommandResultDispatchRequest.Target,
+ legacyAsyncCommandResultDispatchRequest.ExecuteAsync,
+ cancellationToken).ConfigureAwait(false),
+ LegacyAsyncQueryDispatchRequest legacyAsyncQueryDispatchRequest => await ExecuteContextAwareRequestAsync(
+ context,
+ legacyAsyncQueryDispatchRequest.Target,
+ legacyAsyncQueryDispatchRequest.ExecuteAsync,
+ cancellationToken).ConfigureAwait(false),
IRequest => Unit.Value,
_ => _responseFactory(request)
};
- return ValueTask.FromResult((TResponse)response!);
+ return ConvertResponse(request, response);
}
///
@@ -63,4 +88,120 @@ internal sealed class RecordingCqrsRuntime(Func? responseFacto
{
throw new NotSupportedException();
}
+
+ ///
+ /// 将测试替身工厂返回的装箱结果显式还原为目标类型,并在类型不匹配时给出可诊断异常。
+ ///
+ /// 当前请求声明的响应类型。
+ /// 触发响应工厂的请求实例。
+ /// 响应工厂返回的装箱结果。
+ /// 还原后的目标类型响应。
+ ///
+ /// 响应工厂返回 或错误类型,导致无法还原为 。
+ ///
+ private static TResponse ConvertResponse(IRequest request, object? response)
+ {
+ if (response is TResponse typedResponse)
+ {
+ return typedResponse;
+ }
+
+ if (response is null && !typeof(TResponse).IsValueType)
+ {
+ return (TResponse)response!;
+ }
+
+ string actualType = response?.GetType().FullName ?? "null";
+ throw new InvalidOperationException(
+ $"RecordingCqrsRuntime 无法将响应类型从 '{actualType}' 转换为 '{typeof(TResponse).FullName}'。"
+ + $" 请求类型:'{request.GetType().FullName}'。");
+ }
+
+ ///
+ /// 按 bridge handler 语义为 legacy 无返回值命令注入上下文并执行。
+ ///
+ /// 当前运行时接收到的架构上下文。
+ /// 待执行的 legacy 命令桥接请求。
+ /// 桥接后的 响应。
+ private static Unit ExecuteLegacyCommand(
+ ICqrsContext context,
+ LegacyCommandDispatchRequest request)
+ {
+ PrepareTarget(context, request.Command);
+ request.Command.Execute();
+ return Unit.Value;
+ }
+
+ ///
+ /// 按 bridge handler 语义为 legacy 异步无返回值命令注入上下文并执行。
+ ///
+ /// 当前运行时接收到的架构上下文。
+ /// 待执行的 legacy 异步命令桥接请求。
+ /// 调用方传入的取消令牌。
+ /// 表示 bridge 执行完成的异步结果。
+ private static async Task ExecuteLegacyAsyncCommandAsync(
+ ICqrsContext context,
+ LegacyAsyncCommandDispatchRequest request,
+ CancellationToken cancellationToken)
+ {
+ PrepareTarget(context, request.Command);
+ await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
+ return Unit.Value;
+ }
+
+ ///
+ /// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行同步委托。
+ ///
+ /// 当前运行时接收到的架构上下文。
+ /// 需要接收上下文注入的 legacy 目标对象。
+ /// 实际执行 legacy 目标逻辑的同步委托。
+ /// 同步执行结果。
+ private static object? ExecuteContextAwareRequest(
+ ICqrsContext context,
+ object target,
+ Func execute)
+ {
+ PrepareTarget(context, target);
+ return execute();
+ }
+
+ ///
+ /// 按 bridge handler 语义为带返回值 legacy 请求注入上下文并执行异步委托。
+ ///
+ /// 当前运行时接收到的架构上下文。
+ /// 需要接收上下文注入的 legacy 目标对象。
+ /// 实际执行 legacy 目标逻辑的异步委托。
+ /// 调用方传入的取消令牌。
+ /// 异步执行结果。
+ private static async Task ExecuteContextAwareRequestAsync(
+ ICqrsContext context,
+ object target,
+ Func> executeAsync,
+ CancellationToken cancellationToken)
+ {
+ PrepareTarget(context, target);
+ return await executeAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// 模拟 legacy bridge handler 的上下文注入语义,使测试替身与生产桥接行为保持一致。
+ ///
+ /// 当前运行时接收到的架构上下文。
+ /// 即将执行的 legacy 目标对象。
+ private static void PrepareTarget(ICqrsContext context, object target)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(target);
+
+ if (context is not GFramework.Core.Abstractions.Architectures.IArchitectureContext architectureContext)
+ {
+ throw new InvalidOperationException(
+ $"RecordingCqrsRuntime 期望收到 IArchitectureContext,但实际为 '{context.GetType().FullName}'。");
+ }
+
+ if (target is IContextAware contextAware)
+ {
+ contextAware.SetContext(architectureContext);
+ }
+ }
}
diff --git a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs
index 8ac9f002..7781bc73 100644
--- a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs
+++ b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs
@@ -159,6 +159,7 @@ public class AsyncQueryExecutorTests
{
Assert.That(result, Is.EqualTo(64));
Assert.That(runtime.LastRequest, Is.TypeOf());
+ Assert.That(query.ObservedContext, Is.SameAs(expectedContext));
});
}
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index b46e19bb..477ec8f7 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-096`
+- 恢复点编号:`CQRS-REWRITE-RP-097`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #334`
- 当前结论:
@@ -31,8 +31,9 @@ CQRS 迁移与收敛。
- `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
- `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
- `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
- - 当前 `RP-096` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,确认仍显示为 open 的 AI threads 在本地代码中已无新增仍成立的运行时 / 测试 / 文档缺陷,剩余差异主要是 GitHub thread 未 resolve 的状态滞后
-- `ai-plan` active 入口现以 `RP-096` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - `RP-096` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,确认仍显示为 open 的 AI threads 在本地代码中已无新增仍成立的运行时 / 测试 / 文档缺陷,剩余差异主要是 GitHub thread 未 resolve 的状态滞后
+ - 当前 `RP-097` 已继续收口 `PR #334` latest-head nitpick:为 `AsyncQueryExecutorTests` / `CommandExecutorTests` 补齐可观察的上下文保留断言,并让 `RecordingCqrsRuntime` 在测试替身返回错误响应类型时抛出带请求/类型信息的诊断异常
+- `ai-plan` active 入口现以 `RP-097` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
@@ -62,6 +63,8 @@ CQRS 迁移与收敛。
- 远端 `CTRF` 最新汇总为 `2311/2311` passed(run `#1079`, 2026-05-07)
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread
+- `GFramework.Core.Tests` 中 legacy bridge 的“保留上下文”回归现在同时断言 bridge request 类型与目标对象执行期观察到的 `IArchitectureContext`
+- `RecordingCqrsRuntime` 对非 `Unit` 响应已显式校验返回值类型;若测试工厂返回了 `null` 或错误装箱类型,异常会直接指出 request 类型与期望/实际响应类型
## 当前风险
@@ -97,6 +100,14 @@ CQRS 迁移与收敛。
- 备注:确认当前分支对应 `PR #334`;`CodeRabbit` latest review 已 `APPROVED`,但 latest-head 仍显示 `10` 个 open thread、`Greptile` 仍显示 `3` 个 open thread;本地逐项复核后未发现新的仍成立缺陷,最新 CI 测试汇总为 `2311/2311` passed,`MegaLinter` 仅剩 `dotnet-format` restore 环境噪音
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
+- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
+ - 结果:通过,`19/19` passed
+- `python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output `
- 结果:通过
- 备注:确认当前分支对应 `PR #331`,本轮 latest-head open AI feedback 已收敛到 `dotnet pack --no-build`、共享包校验脚本跨平台兼容性与 active 文档 PR 锚点同步
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index f083ca9b..4244a7eb 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,26 @@
## 2026-05-07
+### 阶段:PR #334 nitpick 测试收尾(CQRS-REWRITE-RP-097)
+
+- 继续处理 `PR #334` latest-head review 中仍值得本地吸收的轻量 nitpick,范围限定在 legacy bridge 测试可观察性与测试替身诊断质量:
+ - `AsyncQueryExecutorTests.SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context` 标题声明“保留上下文”,但此前只断言了返回值与 bridge request 类型
+ - `CommandExecutorTests.Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context` 同样缺少可观察的上下文注入断言
+ - `RecordingCqrsRuntime` 直接强转响应对象,若测试工厂回错类型,失败信息不够聚焦
+- 本轮主线程决策:
+ - 为两个 “Preserve_Context” 用例补齐 `ObservedContext` 与 `expectedContext` 的同一实例断言,使测试标题、注释与断言对象保持一致
+ - 让 `RecordingCqrsRuntime` 通过私有 helper 显式执行响应类型还原;当工厂返回 `null` 或错误装箱类型时,抛出包含 request 类型与期望/实际响应类型的 `InvalidOperationException`
+ - 同步刷新 `cqrs-rewrite` active tracking,把本轮 nitpick 收敛与验证结果记录为新的恢复锚点 `RP-097`
+- 本轮权威验证:
+ - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
+ - 结果:通过,`19/19` passed
+ - `python3 scripts/license-header.py --check`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+
### 阶段:PR #334 latest-head review 复核(CQRS-REWRITE-RP-096)
- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并读取 `/tmp/current-pr-review.json` 中的 `review_agents`、`latest_commit_review`、`megalinter_report` 与 `test_reports`
From ffb0a8aff5c704c27ddf00c2914b071ff8ba4911 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 7 May 2026 20:35:47 +0800
Subject: [PATCH 7/7] =?UTF-8?q?fix(core):=20=E6=94=B6=E7=AA=84=20legacy=20?=
=?UTF-8?q?bridge=20=E4=B8=8A=E4=B8=8B=E6=96=87=E5=9B=9E=E9=80=80=E5=BC=82?=
=?UTF-8?q?=E5=B8=B8=E8=BE=B9=E7=95=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 LegacyCqrsDispatchHelper 仅在上下文缺失时回退,避免吞掉真实 InvalidOperationException
- 补充 CommandExecutor 与 QueryExecutor 相关回归测试,覆盖 fallback 与异常冒泡语义
- 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #334 本轮复核与验证结果
---
.../Command/CommandExecutorTests.cs | 90 +++++++++++++++++++
.../Cqrs/LegacyCqrsDispatchHelper.cs | 24 ++++-
.../todos/cqrs-rewrite-migration-tracking.md | 26 ++++--
.../traces/cqrs-rewrite-migration-trace.md | 28 ++++++
4 files changed, 162 insertions(+), 6 deletions(-)
diff --git a/GFramework.Core.Tests/Command/CommandExecutorTests.cs b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
index aad98ca3..dd08cf80 100644
--- a/GFramework.Core.Tests/Command/CommandExecutorTests.cs
+++ b/GFramework.Core.Tests/Command/CommandExecutorTests.cs
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Command;
+using GFramework.Core.Rule;
using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Command;
@@ -76,6 +77,41 @@ public class CommandExecutorTests
Assert.Throws(() => _commandExecutor.Send(null!));
}
+ ///
+ /// 验证当 legacy 命令没有可用上下文时,会安全回退到本地直接执行路径。
+ ///
+ [Test]
+ public void Send_Should_Fall_Back_To_Legacy_Execution_When_Context_IsMissing()
+ {
+ var runtime = new RecordingCqrsRuntime();
+ var executor = new CommandExecutor(runtime);
+ var command = new MissingContextLegacyCommand();
+
+ executor.Send(command);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.Executed, Is.True);
+ Assert.That(runtime.LastRequest, Is.Null);
+ });
+ }
+
+ ///
+ /// 验证非“缺上下文”类型的 不会被 bridge 回退逻辑误吞掉。
+ ///
+ [Test]
+ public void Send_Should_Propagate_InvalidOperationException_When_ContextAware_Target_Throws_Unexpected_Error()
+ {
+ var runtime = new RecordingCqrsRuntime();
+ var executor = new CommandExecutor(runtime);
+ var command = new ThrowingLegacyCommand();
+
+ Assert.That(
+ () => executor.Send(command),
+ Throws.InvalidOperationException.With.Message.EqualTo(ThrowingLegacyCommand.ExceptionMessage));
+ Assert.That(runtime.LastRequest, Is.Null);
+ }
+
///
/// 验证 legacy 同步命令桥接会在线程池上等待 runtime,
/// 避免直接继承调用方当前的同步上下文。
@@ -184,4 +220,58 @@ public class CommandExecutorTests
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
{
}
+
+ ///
+ /// 用于验证缺少上下文时仍会走本地 fallback 的测试命令。
+ ///
+ private sealed class MissingContextLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
+ {
+ ///
+ /// 获取命令是否已经执行。
+ ///
+ public bool Executed { get; private set; }
+
+ ///
+ public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ }
+
+ ///
+ public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
+ {
+ throw new InvalidOperationException("Architecture context has not been set. Call SetContext before accessing the context.");
+ }
+
+ ///
+ public void Execute()
+ {
+ Executed = true;
+ }
+ }
+
+ ///
+ /// 用于验证 bridge 上下文解析不会吞掉意外运行时错误的测试命令。
+ ///
+ private sealed class ThrowingLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
+ {
+ internal const string ExceptionMessage = "Unexpected context failure.";
+
+ ///
+ public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ }
+
+ ///
+ public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
+ {
+ throw new InvalidOperationException(ExceptionMessage);
+ }
+
+ ///
+ public void Execute()
+ {
+ }
+ }
}
diff --git a/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs b/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
index 5fb6fec0..9d2f77f3 100644
--- a/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
+++ b/GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
@@ -48,12 +48,34 @@ internal static class LegacyCqrsDispatchHelper
context = contextAware.GetContext();
return true;
}
- catch (InvalidOperationException)
+ catch (InvalidOperationException exception) when (IsMissingContextException(exception))
{
return false;
}
}
+ ///
+ /// 判断当前 是否表示 legacy 目标尚未具备可桥接的架构上下文。
+ ///
+ /// 由 抛出的异常。
+ ///
+ /// 仅当异常明确表示“上下文尚未设置”或“当前没有活动上下文”时返回 ;
+ /// 其他运行时错误必须继续向上传播,避免把真实故障误判为可安全回退。
+ ///
+ private static bool IsMissingContextException(InvalidOperationException exception)
+ {
+ ArgumentNullException.ThrowIfNull(exception);
+
+ return string.Equals(
+ exception.Message,
+ "Architecture context has not been set. Call SetContext before accessing the context.",
+ StringComparison.Ordinal)
+ || string.Equals(
+ exception.Message,
+ "No active architecture context is currently bound.",
+ StringComparison.Ordinal);
+ }
+
///
/// 同步等待统一 CQRS runtime 完成无返回值请求。
///
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index 477ec8f7..d2d58068 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-097`
+- 恢复点编号:`CQRS-REWRITE-RP-098`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #334`
- 当前结论:
@@ -32,8 +32,9 @@ CQRS 迁移与收敛。
- `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
- `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
- `RP-096` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,确认仍显示为 open 的 AI threads 在本地代码中已无新增仍成立的运行时 / 测试 / 文档缺陷,剩余差异主要是 GitHub thread 未 resolve 的状态滞后
- - 当前 `RP-097` 已继续收口 `PR #334` latest-head nitpick:为 `AsyncQueryExecutorTests` / `CommandExecutorTests` 补齐可观察的上下文保留断言,并让 `RecordingCqrsRuntime` 在测试替身返回错误响应类型时抛出带请求/类型信息的诊断异常
-- `ai-plan` active 入口现以 `RP-097` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - `RP-097` 已继续收口 `PR #334` latest-head nitpick:为 `AsyncQueryExecutorTests` / `CommandExecutorTests` 补齐可观察的上下文保留断言,并让 `RecordingCqrsRuntime` 在测试替身返回错误响应类型时抛出带请求/类型信息的诊断异常
+ - 当前 `RP-098` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,并收口 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 过宽吞掉 `InvalidOperationException` 的真实运行时诊断退化问题;现在仅把“上下文尚未就绪”视为允许 fallback 的信号,并为 fallback / 异常冒泡分别补齐回归测试
+- `ai-plan` active 入口现以 `RP-098` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
@@ -55,13 +56,15 @@ CQRS 迁移与收敛。
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o ` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
-- `PR #334` 在 `2026-05-07` 的 latest-head review 仍显示 `CodeRabbit 10` / `Greptile 3` 个 open thread,但本地逐项复核后未发现新的仍成立代码或文档缺陷;当前差异主要来自已实质修复但尚未 resolve 的 thread 状态
+- `PR #334` 在 `2026-05-07` 的 latest-head review 当前显示 `CodeRabbit 10` / `Greptile 5` 个 open thread;本轮再次复核后确认其中大部分仍是已实质修复但未 resolve 的 stale thread,仅 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 的异常边界仍需要继续收口
- benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型
- `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered`
- `BenchmarkDotNet` 在当前 agent 沙箱里会因自动生成的 bootstrap 脚本异常失败;同一 `dotnet run --no-build` 命令在沙箱外执行通过,因此本轮以沙箱外结果作为 benchmark 权威验证
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
- 远端 `CTRF` 最新汇总为 `2311/2311` passed(run `#1079`, 2026-05-07)
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
+- `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 现在只会把“Context 尚未设置”或“当前没有活动上下文”识别为可安全 fallback 的缺上下文信号;其他 `InvalidOperationException` 将继续向上传播,避免把真实运行时故障误判成 legacy 直执行场景
+- `CommandExecutorTests` 已新增“缺上下文继续 fallback”和“意外 `InvalidOperationException` 必须冒泡”的回归,防止后续再次放宽该异常过滤面
- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread
- `GFramework.Core.Tests` 中 legacy bridge 的“保留上下文”回归现在同时断言 bridge request 类型与目标对象执行期观察到的 `IArchitectureContext`
- `RecordingCqrsRuntime` 对非 `Unit` 响应已显式校验返回值类型;若测试工厂返回了 `null` 或错误装箱类型,异常会直接指出 request 类型与期望/实际响应类型
@@ -104,6 +107,19 @@ CQRS 迁移与收敛。
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests"`
- 结果:通过,`19/19` passed
+- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
+ - 结果:通过
+ - 备注:确认当前分支对应 `PR #334`;最新 review 仍为 `CodeRabbit APPROVED (2026-05-07T12:20:24Z)`,latest-head 显示 `CodeRabbit 10` / `Greptile 5` open thread;本轮接受并修复的仍成立问题收敛到 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 的过宽异常吞掉逻辑
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests"`
+ - 结果:通过,`25/25` passed
+- `python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
- `python3 scripts/license-header.py --check`
- 结果:通过
- `git diff --check`
@@ -159,7 +175,7 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #334` stale review threads,或等待下一次 head 更新后再次用 `$gframework-pr-review` 复核状态是否自动收敛
+1. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #334` stale review threads,尤其是仍停留在旧 head 上的 CodeRabbit / Greptile open thread;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核
2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `stream pipeline` 或 `notification publisher` 策略中选择一个独立切片推进
3. 若继续收敛 legacy Core CQRS,可评估是否补一个 `IMediator` 风格 facade,而不是继续扩大 `ArchitectureContext` 兼容入口的职责
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 4244a7eb..a76a5bcb 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,34 @@
## 2026-05-07
+### 阶段:PR #334 latest-head helper 异常边界收口(CQRS-REWRITE-RP-098)
+
+- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并重新核对 `/tmp/current-pr-review.json` 中最新 open thread:
+ - 当前公开 PR 仍为 `PR #334`
+ - `CodeRabbit` 最新 review 在 `2026-05-07T12:20:24Z` 为 `APPROVED`
+ - latest-head 当前显示 `CodeRabbit 10` / `Greptile 5` 个 open thread
+- 本轮逐条回到本地代码后,确认大多数 open thread 仍是 stale 状态;唯一继续成立的问题集中在 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)`:
+ - 该 helper 之前会把 `IContextAware.GetContext()` 抛出的任意 `InvalidOperationException` 都吞掉并回退到 legacy 直执行
+ - 这会把真实运行时故障误判为“上下文未就绪”,导致 bridge 路径悄悄绕过统一 runtime,退化为难以诊断的行为差异
+- 本轮主线程决策:
+ - 将异常过滤收窄为只接受两类缺上下文信号:`Architecture context has not been set...` 与 `No active architecture context is currently bound.`
+ - 其他 `InvalidOperationException` 一律继续向上传播,避免掩盖容器、生命周期或自定义 `GetContext()` 内的真实错误
+ - 在 `CommandExecutorTests` 中新增两条回归:一条验证缺上下文时仍会 fallback 到 legacy 直执行;一条验证意外 `InvalidOperationException` 不会被 bridge 逻辑静默吞掉
+ - 同步刷新 `cqrs-rewrite` active tracking,把本轮修复记录为新的恢复锚点 `RP-098`
+- 本轮权威验证:
+ - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
+ - 结果:通过
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests"`
+ - 结果:通过,`25/25` passed
+ - `python3 scripts/license-header.py --check`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+
### 阶段:PR #334 nitpick 测试收尾(CQRS-REWRITE-RP-097)
- 继续处理 `PR #334` latest-head review 中仍值得本地吸收的轻量 nitpick,范围限定在 legacy bridge 测试可观察性与测试替身诊断质量: