From babd132e8131ae825b936ec60253c7b4fc93a353 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 11 May 2026 13:02:01 +0800
Subject: [PATCH] =?UTF-8?q?docs(cqrs):=20=E6=94=B6=E5=8F=A3=E6=89=B9?=
=?UTF-8?q?=E5=A4=84=E7=90=86=E5=89=A9=E4=BD=99=E6=96=87=E6=A1=A3=E4=B8=8E?=
=?UTF-8?q?=E8=BF=BD=E8=B8=AA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 NotificationLifetime 基准并补充验证结果
- 更新 CQRS README 与 legacy Command/Query 迁移说明
- 补充 registration fallback 回归测试并同步 ai-plan 恢复点
---
.../NotificationLifetimeBenchmarks.cs | 315 ++++++++++++++++++
GFramework.Cqrs.Benchmarks/README.md | 6 +-
.../Cqrs/CqrsRegistrationServiceTests.cs | 108 ++++++
GFramework.Cqrs/README.md | 18 +-
.../todos/cqrs-rewrite-migration-tracking.md | 99 ++++--
.../traces/cqrs-rewrite-migration-trace.md | 52 +++
docs/zh-CN/core/command.md | 26 +-
docs/zh-CN/core/query.md | 19 +-
8 files changed, 600 insertions(+), 43 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs
new file mode 100644
index 00000000..ac83127a
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs
@@ -0,0 +1,315 @@
+// 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;
+
+///
+/// 对比单处理器 notification publish 在不同 handler 生命周期下的额外开销。
+///
+///
+/// 当前矩阵覆盖 Singleton、Scoped 与 Transient。
+/// 其中 Scoped 会在每次 notification publish 前显式创建并释放真实的 DI 作用域,
+/// 避免把 scoped handler 错误地压到根容器解析而扭曲生命周期对照。
+///
+[Config(typeof(Config))]
+public class NotificationLifetimeBenchmarks
+{
+ private MicrosoftDiContainer _container = null!;
+ private ICqrsRuntime? _runtime;
+ private ScopedBenchmarkContainer? _scopedContainer;
+ private ICqrsRuntime? _scopedRuntime;
+ private ServiceProvider _serviceProvider = null!;
+ private IPublisher? _publisher;
+ private BenchmarkNotificationHandler _baselineHandler = null!;
+ private BenchmarkNotification _notification = null!;
+ private ILogger _runtimeLogger = null!;
+
+ ///
+ /// 控制当前 benchmark 使用的 handler 生命周期。
+ ///
+ [Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
+ public HandlerLifetime Lifetime { get; set; }
+
+ ///
+ /// 可公平比较的 benchmark handler 生命周期集合。
+ ///
+ public enum HandlerLifetime
+ {
+ ///
+ /// 复用单个 handler 实例。
+ ///
+ Singleton,
+
+ ///
+ /// 每次 publish 在显式作用域内解析并复用 handler 实例。
+ ///
+ Scoped,
+
+ ///
+ /// 每次 publish 都重新解析新的 handler 实例。
+ ///
+ Transient
+ }
+
+ ///
+ /// 配置 notification lifetime benchmark 的公共输出格式。
+ ///
+ private sealed class Config : ManualConfig
+ {
+ public Config()
+ {
+ AddJob(Job.Default);
+ AddColumnProvider(DefaultColumnProviders.Instance);
+ AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationLifetime"));
+ AddDiagnoser(MemoryDiagnoser.Default);
+ WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
+ }
+ }
+
+ ///
+ /// 构建当前生命周期下的 GFramework 与 MediatR notification 对照宿主。
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
+ {
+ MinLevel = LogLevel.Fatal
+ };
+ Fixture.Setup($"NotificationLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+
+ _baselineHandler = new BenchmarkNotificationHandler();
+ _notification = new BenchmarkNotification(Guid.NewGuid());
+ _runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationLifetimeBenchmarks) + "." + Lifetime);
+
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ RegisterGFrameworkHandler(container, Lifetime);
+ });
+
+ if (Lifetime != HandlerLifetime.Scoped)
+ {
+ _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(_container, _runtimeLogger);
+ }
+ else
+ {
+ _scopedContainer = new ScopedBenchmarkContainer(_container);
+ _scopedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(_scopedContainer, _runtimeLogger);
+ }
+
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(NotificationLifetimeBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
+ ResolveMediatRLifetime(Lifetime));
+ if (Lifetime != HandlerLifetime.Scoped)
+ {
+ _publisher = _serviceProvider.GetRequiredService();
+ }
+ }
+
+ ///
+ /// 释放当前生命周期矩阵持有的 benchmark 宿主资源。
+ ///
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ try
+ {
+ BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
+ }
+ finally
+ {
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+ }
+ }
+
+ ///
+ /// 直接调用 handler,作为不同生命周期矩阵下的 publish 额外开销 baseline。
+ ///
+ [Benchmark(Baseline = true)]
+ public ValueTask PublishNotification_Baseline()
+ {
+ return _baselineHandler.Handle(_notification, CancellationToken.None);
+ }
+
+ ///
+ /// 通过 GFramework.CQRS runtime 发布 notification。
+ ///
+ [Benchmark]
+ public ValueTask PublishNotification_GFrameworkCqrs()
+ {
+ if (Lifetime == HandlerLifetime.Scoped)
+ {
+ return PublishScopedGFrameworkNotificationAsync(
+ _scopedRuntime!,
+ _scopedContainer!,
+ _notification,
+ CancellationToken.None);
+ }
+
+ return _runtime!.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
+ }
+
+ ///
+ /// 通过 MediatR 发布 notification,作为外部对照。
+ ///
+ [Benchmark]
+ public Task PublishNotification_MediatR()
+ {
+ if (Lifetime == HandlerLifetime.Scoped)
+ {
+ return PublishScopedMediatRNotificationAsync(_serviceProvider, _notification, CancellationToken.None);
+ }
+
+ return _publisher!.Publish(_notification, CancellationToken.None);
+ }
+
+ ///
+ /// 按生命周期把 benchmark notification handler 注册到 GFramework 容器。
+ ///
+ /// 当前 benchmark 拥有并负责释放的容器。
+ /// 待比较的 handler 生命周期。
+ private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ switch (lifetime)
+ {
+ case HandlerLifetime.Singleton:
+ container.RegisterSingleton, BenchmarkNotificationHandler>();
+ return;
+
+ case HandlerLifetime.Scoped:
+ container.RegisterScoped, BenchmarkNotificationHandler>();
+ return;
+
+ case HandlerLifetime.Transient:
+ container.RegisterTransient, BenchmarkNotificationHandler>();
+ 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.Scoped => ServiceLifetime.Scoped,
+ HandlerLifetime.Transient => ServiceLifetime.Transient,
+ _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
+ };
+ }
+
+ ///
+ /// 在真实的 publish 级作用域内执行一次 GFramework.CQRS notification 分发。
+ ///
+ /// 复用的 scoped benchmark runtime。
+ /// 负责为每次 publish 激活独立作用域的只读容器适配层。
+ /// 要发布的 notification。
+ /// 取消令牌。
+ /// 代表当前 publish 完成的值任务。
+ ///
+ /// notification lifetime benchmark 只关心 handler 解析和 publish 本身的热路径,
+ /// 因此这里复用同一个 runtime,但在每次调用前后显式创建并释放新的 DI 作用域,
+ /// 让 scoped handler 真正绑定到 publish 边界。
+ ///
+ private static async ValueTask PublishScopedGFrameworkNotificationAsync(
+ ICqrsRuntime runtime,
+ ScopedBenchmarkContainer scopedContainer,
+ BenchmarkNotification notification,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(runtime);
+ ArgumentNullException.ThrowIfNull(scopedContainer);
+ ArgumentNullException.ThrowIfNull(notification);
+
+ using var scopeLease = scopedContainer.EnterScope();
+ await runtime.PublishAsync(BenchmarkContext.Instance, notification, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// 在真实的 publish 级作用域内执行一次 MediatR notification 分发。
+ ///
+ /// 当前 benchmark 的根 。
+ /// 要发布的 notification。
+ /// 取消令牌。
+ /// 代表当前 publish 完成的任务。
+ ///
+ /// 这里显式从新的 scope 解析 ,确保 Scoped handler 与依赖绑定到 publish 边界。
+ ///
+ private static async Task PublishScopedMediatRNotificationAsync(
+ ServiceProvider rootServiceProvider,
+ BenchmarkNotification notification,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(rootServiceProvider);
+ ArgumentNullException.ThrowIfNull(notification);
+
+ using var scope = rootServiceProvider.CreateScope();
+ var publisher = scope.ServiceProvider.GetRequiredService();
+ await publisher.Publish(notification, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Benchmark notification。
+ ///
+ /// 通知标识。
+ public sealed record BenchmarkNotification(Guid Id) :
+ GFramework.Cqrs.Abstractions.Cqrs.INotification,
+ MediatR.INotification;
+
+ ///
+ /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。
+ ///
+ public sealed class BenchmarkNotificationHandler :
+ GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler,
+ MediatR.INotificationHandler
+ {
+ ///
+ /// 处理 GFramework.CQRS notification。
+ ///
+ public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(notification);
+ cancellationToken.ThrowIfCancellationRequested();
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ /// 处理 MediatR notification。
+ ///
+ Task MediatR.INotificationHandler.Handle(
+ BenchmarkNotification notification,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(notification);
+ cancellationToken.ThrowIfCancellationRequested();
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index 408ff3ee..69038b45 100644
--- a/GFramework.Cqrs.Benchmarks/README.md
+++ b/GFramework.Cqrs.Benchmarks/README.md
@@ -38,13 +38,16 @@
- `Messaging/StreamStartupBenchmarks.cs`
- `Initialization` 与 `ColdStart` 两组下,`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR`
- 其中 `ColdStart` 的边界是“新宿主 + 首个元素命中”,不是完整枚举整个 stream
-- notification
+- notification steady-state
- `Messaging/NotificationBenchmarks.cs`
- 单处理器 publish 下,`GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR`
- `Messaging/NotificationLifetimeBenchmarks.cs`
- 单处理器 publish 在 `Singleton / Scoped / Transient` 三类 handler 生命周期下的 baseline、`GFramework.Cqrs` 与 `MediatR` 对照
- `Messaging/NotificationFanOutBenchmarks.cs`
- 固定 `4 handler` fan-out 下的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator`、`MediatR`
+- notification startup
+ - `Messaging/NotificationStartupBenchmarks.cs`
+ - `Initialization` 与 `ColdStart` 两组下,`GFramework.Cqrs`、NuGet `Mediator`、`MediatR`
## 最小使用方式
@@ -95,6 +98,5 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- 当前没有 stream 版的 NuGet `Mediator` source-generated concrete path 对照;stream steady-state、lifetime、startup 现在都只覆盖 `GFramework.Cqrs` 与 `MediatR`
- 当前没有 request 生命周期下的 NuGet `Mediator` compile-time lifetime 矩阵;`RequestLifetimeBenchmarks` 只覆盖 `GFramework.Cqrs` 与 `MediatR`
-- 当前没有 notification startup / cold-start benchmark;notification 只覆盖 steady-state 单处理器、生命周期、固定 `4 handler` fan-out
- 当前没有 notification fan-out 的生命周期矩阵;`NotificationFanOutBenchmarks` 只覆盖固定 `4 handler` 的已装配宿主
- 当前没有 stream pipeline benchmark;现有 pipeline coverage 仅限 request
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs
index e80b5e3f..35f15736 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs
@@ -87,17 +87,125 @@ internal sealed class CqrsRegistrationServiceTests
});
}
+ ///
+ /// 验证当 缺失时,协调器会退化到 作为稳定程序集键。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Fallback_To_Simple_Name_When_Full_Name_Is_Missing()
+ {
+ var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
+ var registrar = new Mock(MockBehavior.Strict);
+ var firstAssembly = CreateAssembly(
+ assemblyFullName: null,
+ assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
+ assemblyDisplayName: "DisplayName-A");
+ var secondAssembly = CreateAssembly(
+ assemblyFullName: null,
+ assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
+ assemblyDisplayName: "DisplayName-B");
+ IEnumerable? registeredAssemblies = null;
+
+ registrar
+ .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()))
+ .Callback>(assemblies => registeredAssemblies = assemblies.ToArray());
+
+ var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
+
+ service.RegisterHandlers([firstAssembly.Object]);
+ service.RegisterHandlers([secondAssembly.Object]);
+
+ registrar.Verify(
+ static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()),
+ Times.Once);
+ Assert.Multiple(() =>
+ {
+ Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
+ var debugMessages = logger.Logs
+ .Where(static log => log.Level == LogLevel.Debug)
+ .Select(static log => log.Message)
+ .ToArray();
+ Assert.That(debugMessages, Has.Length.EqualTo(1));
+ Assert.That(debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.SimpleNameFallback"));
+ Assert.That(debugMessages[0], Does.Not.Contain("DisplayName-B"));
+ });
+ }
+
+ ///
+ /// 验证当 与 均缺失时,
+ /// 协调器会退化到 结果作为稳定程序集键。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Fallback_To_ToString_When_Full_Name_And_Simple_Name_Are_Missing()
+ {
+ var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
+ var registrar = new Mock(MockBehavior.Strict);
+ const string assemblyDisplayName = "GFramework.Cqrs.Tests.ToStringFallback";
+ var firstAssembly = CreateAssembly(
+ assemblyFullName: null,
+ assemblySimpleName: null,
+ assemblyDisplayName: assemblyDisplayName);
+ var secondAssembly = CreateAssembly(
+ assemblyFullName: null,
+ assemblySimpleName: null,
+ assemblyDisplayName: assemblyDisplayName);
+ IEnumerable? registeredAssemblies = null;
+
+ registrar
+ .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()))
+ .Callback>(assemblies => registeredAssemblies = assemblies.ToArray());
+
+ var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
+
+ service.RegisterHandlers([firstAssembly.Object]);
+ service.RegisterHandlers([secondAssembly.Object]);
+
+ registrar.Verify(
+ static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()),
+ Times.Once);
+ Assert.Multiple(() =>
+ {
+ Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
+ var debugMessages = logger.Logs
+ .Where(static log => log.Level == LogLevel.Debug)
+ .Select(static log => log.Message)
+ .ToArray();
+ Assert.That(debugMessages, Has.Length.EqualTo(1));
+ Assert.That(debugMessages[0], Does.Contain(assemblyDisplayName));
+ Assert.That(debugMessages[0], Does.Contain("already registered"));
+ });
+ }
+
///
/// 创建一个带稳定程序集键的程序集 mock,用于模拟不同 实例表示同一程序集的场景。
///
/// 要返回的程序集完整名称。
/// 配置好完整名称的程序集 mock。
private static Mock CreateAssembly(string assemblyFullName)
+ {
+ return CreateAssembly(assemblyFullName, assemblySimpleName: null, assemblyDisplayName: assemblyFullName);
+ }
+
+ ///
+ /// 创建一个可配置程序集元数据退化路径的程序集 mock,用于验证稳定程序集键的回退顺序。
+ ///
+ /// 要返回的程序集完整名称;为 时模拟缺失完整名称。
+ /// 要返回的程序集简单名称;为 时模拟缺失简单名称。
+ /// 当需要退化到 时返回的显示名称。
+ /// 配置好程序集元数据的程序集 mock。
+ private static Mock CreateAssembly(string? assemblyFullName, string? assemblySimpleName, string assemblyDisplayName)
{
var assembly = new Mock();
+ var assemblyName = new AssemblyName();
assembly
.SetupGet(static currentAssembly => currentAssembly.FullName)
.Returns(assemblyFullName);
+ assemblyName.Name = assemblySimpleName;
+ assembly
+ .Setup(static currentAssembly => currentAssembly.GetName())
+ .Returns(assemblyName);
+ assembly
+ .Setup(static currentAssembly => currentAssembly.ToString())
+ .Returns(assemblyDisplayName);
return assembly;
}
diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md
index 238aa892..4c33f27b 100644
--- a/GFramework.Cqrs/README.md
+++ b/GFramework.Cqrs/README.md
@@ -72,7 +72,7 @@ dotnet add package GeWuYou.GFramework.Cqrs
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
```
-如果你希望减少处理器注册时的反射扫描,再额外安装:
+如果你希望把可静态表达的 handler 注册与 request / stream invoker 元数据前移到编译期,再额外安装:
```bash
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
@@ -116,7 +116,9 @@ using GFramework.Cqrs.Extensions;
var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
-在 `ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync`、`SendQueryAsync`、`PublishAsync` 和 `CreateStream`。
+在 `ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync`、`SendAsync`、`SendQueryAsync`、`PublishAsync` 和 `CreateStream`。
+
+如果你走标准 `GFramework.Core` 架构启动路径,`CqrsRuntimeModule` 会自动创建 runtime 并接线默认注册流程;只有在裸容器、测试宿主或自定义组合根里,才需要显式补齐 runtime、publisher 策略或额外程序集注册。
## 运行时行为
@@ -126,6 +128,8 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
- 通知分发
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`。
+ - notification publish 不存在 generated invoker 通道;它始终基于当前已注册的 `INotificationHandler<>` 集合和选定的 `INotificationPublisher` 策略执行。
+ - 默认 runtime 只消费一个 `INotificationPublisher`;如果容器里已经存在该注册,再调用 `UseNotificationPublisher(...)`、`UseNotificationPublisher()`、`UseSequentialNotificationPublisher()` 或 `UseTaskWhenAllNotificationPublisher()` 会直接报错,而不是按“后注册覆盖前注册”处理。
- 内置 notification publisher 的推荐选择如下:
| 策略 | 推荐场景 | 执行顺序 | 失败语义 | 备注 |
@@ -134,7 +138,7 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
| `TaskWhenAllNotificationPublisher` | 需要让全部处理器并行完成,并在结束后统一观察失败或取消 | 不保证顺序 | 不会在首个失败时停止其余处理器;会聚合最终异常或取消结果 | 更适合语义补齐,不是性能开关 |
| `UseNotificationPublisher(...)` / `UseNotificationPublisher()` | 需要接入仓库外的自定义策略或第三方策略 | 取决于具体实现 | 取决于具体实现 | 前者复用现成实例,后者让容器负责单例生命周期 |
- - 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景。
+ - 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景,而不是把 publish 切成另一条 generated 或更快的分发通道。
如果你需要显式保留默认顺序语义,也可以在组合根里直接声明:
@@ -191,18 +195,20 @@ container.UseNotificationPublisher();
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时,registrar 会把对应 descriptor 元数据接线到 runtime 缓存。
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
+- generated invoker 只覆盖 request 与 stream 两类单次分发元数据;`INotificationHandler<>` 仍然只参与 registry / fallback 注册,通知分发本身继续由 runtime 解析出的 handler 集合和 `INotificationPublisher` 策略决定。
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
-- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
-- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。
+- `CqrsReflectionFallbackAttribute` 可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
+- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker、空 fallback 元数据,或生成注册器整体不可用时,才会退回整程序集扫描。
- 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。
如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。
## 适用边界
-- 这个包是默认实现,不是“纯契约包”。
+- 这个包是默认实现,不是“纯契约包”;如果你只需要共享请求/处理器契约,请停在 `GeWuYou.GFramework.Cqrs.Abstractions`。
- 处理器基类依赖 runtime 在分发前注入上下文,不适合脱离 dispatcher 直接手动实例化后调用。
- README 中的消息基类和 handler 基类位于 `GFramework.Cqrs`,接口契约位于 `GFramework.Cqrs.Abstractions`;最小示例通常需要同时引入这两个命名空间层级。
+- 如果你的目标只是“先用起来”,优先沿用 `ArchitectureContext` / `IContextAware` 的统一入口;只有在需要更换通知策略、接入额外程序集或搭裸容器测试时,再显式配置组合根。
## 文档入口
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 951e3c88..9a8beb55 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
@@ -12,57 +12,94 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-132`
+- 恢复点编号:`CQRS-REWRITE-RP-133`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #347`
- 当前结论:
- - 已用 `$gframework-pr-review` 重新抓取并复核 `PR #347` 的 latest-head review,当前仍成立的代码问题已收口到
- `GFramework.Cqrs.Benchmarks` 单模块与 `ai-plan/public/cqrs-rewrite/**` 恢复入口。
- - `Program.cs` 现在按“任意已生效的 artifacts 隔离配置”而不是“仅命令行 `--artifacts-suffix`”决定是否重启隔离宿主;
- 同时新增“目标宿主目录不得等于或嵌套在当前宿主输出目录内”的防守式校验,避免 `host/host/...` 递归膨胀。
- - `RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 的 `Scoped` 路径改为复用单个 scoped runtime /
- dispatcher,只在每次 benchmark 调用时显式创建并释放真实 DI scope,避免把 runtime 构造常量成本混进生命周期矩阵。
- - `ScopedBenchmarkContainer` 已补齐只读适配语义与作用域租约的 XML 合同说明,避免 PR review 再次停留在“公开成员文档不完整”。
- - active tracking / trace 已完成瘦身:历史长流水迁移到 `archive/` 新文件,当前 active 入口只保留恢复点、风险、
- 权威验证与下一步。
+ - 本轮按 `$gframework-batch-boot` 协调多波 non-conflicting subagent,基线固定为
+ `origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`。
+ - 本轮停止继续扩 batch 的主信号是 `reviewability / context-budget`,不是 `50` 文件阈值;
+ 自然停点时累计 branch diff 约为 `12 files`,仍明显低于阈值。
+ - CQRS runtime / tests 侧已补齐并提交:
+ - `CqrsNotificationPublisherTests` 锁定“多 publisher 报错”与“单 dispatcher 内 publisher 缓存复用”
+ - `CqrsGeneratedRequestInvokerProviderTests` 与 `CqrsHandlerRegistrar` 收口 generated descriptor 的异常枚举、
+ 坏元数据与重复 pair 回退契约
+ - `CqrsDispatcherCacheTests` 锁定 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
+ - benchmark 侧已补齐并提交:
+ - `RequestStartupBenchmarks` 的 `Mediator` startup 对照
+ - `StreamStartupBenchmarks`
+ - `NotificationStartupBenchmarks`
+ - `GFramework.Cqrs.Benchmarks/README.md` 的 current coverage / gap 收口
+ - 文档与恢复入口侧已补齐并提交:
+ - `GFramework.Cqrs/README.md`
+ - `docs/zh-CN/core/cqrs.md`
+ - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
+ - `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
+ - 当前尚未提交的收尾切片仅剩:
+ - `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs`
+ - `GFramework.Cqrs/README.md`
+ - `docs/zh-CN/core/command.md`
+ - `docs/zh-CN/core/query.md`
+ - 本 tracking / trace 文件本身
## 当前活跃事实
- 当前分支:`feat/cqrs-optimization`
- 当前 PR:`PR #347`
- 当前写面:
- - `GFramework.Cqrs.Benchmarks/Program.cs`
- `GFramework.Cqrs.Benchmarks/README.md`
- - `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`
- - `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs`
- - `GFramework.Cqrs.Benchmarks/Messaging/ScopedBenchmarkContainer.cs`
- - `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs`
+ - `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
+ - `GFramework.Cqrs.Benchmarks/Messaging/NotificationStartupBenchmarks.cs`
+ - `GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs`
+ - `GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs`
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs`
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs`
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs`
+ - `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
+ - `GFramework.Cqrs/README.md`
- `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`
- `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
- `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-migration-tracking-history-through-rp131.md`
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-migration-trace-history-through-rp131.md`
+ - `docs/zh-CN/core/command.md`
+ - `docs/zh-CN/core/cqrs.md`
+ - `docs/zh-CN/core/query.md`
+ - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 当前基线:
- - `RequestLifetimeBenchmarks.SendRequest_GFrameworkCqrs` short-job 当前约为
- `Singleton 52.69 ns / 32 B`、`Transient 57.88 ns / 56 B`、`Scoped 144.72 ns / 368 B`
- - `StreamLifetimeBenchmarks.Stream_GFramework*` short-job 当前约为
- `Scoped + FirstItem 266.7~267.0 ns / 792 B`、
- `Scoped + DrainAll 331.6~332.2 ns / 856 B`
- - 两条并发 smoke 均已落到独立的
- `BenchmarkDotNet.Artifacts/pr347-req-scoped/host/...` 与
- `BenchmarkDotNet.Artifacts/pr347-stream-scoped/host/...`
+ - 本轮 batch 启动前,分支相对基线的累计 diff 为 `0 files / 0 lines`
+ - 当前自然停点时,累计 diff 约为 `12 files`
+ - 本轮新增 benchmark smoke 结果:
+ - `RequestStartupBenchmarks`
+ - `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
+ - `ColdStart_Mediator 110.867 us / 57872 B`
+ - `ColdStart_MediatR 679.103 us / 606256 B`
+ - `StreamStartupBenchmarks`
+ - `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
+ - `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
+ - `ColdStart_MediatR 933.87 us / 678992 B`
+ - `NotificationStartupBenchmarks`
+ - `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
+ - `ColdStart_Mediator 136.08 us / 62512 B`
+ - `ColdStart_MediatR 1.379 ms / 719056 B`
## 当前风险
-- `Program.cs` 的“嵌套目标目录保护”只覆盖当前宿主目录与隔离宿主目录关系;若后续再扩展更多自定义 artifacts 入口,
- 仍需保持同一层防守式校验,避免配置分叉。
-- `ScopedBenchmarkContainer` 现在明确禁止重叠 active scope;若后续 benchmark 引入同一 runtime 的并行枚举或嵌套调用,
- 需要新的宿主模型,不能直接突破当前只读适配器的约束。
-- 本轮 benchmark 结果仍是 `job short + 1 iteration` smoke,用于证明路径正确与相对量级,不应用作稳定性能结论。
+- `NotificationLifetimeBenchmarks` 当前已跑完整默认作业,但还没并入提交;若继续新开 batch,未提交面会明显降低可审查性。
+- `RequestStartup` 的提交 `8990749d` 连带带入了 `CqrsDispatcherCacheTests.cs`;虽然两条切片均有效且已验证通过,但提交边界不再严格对应单个 ownership slice。
+- startup 与 lifetime benchmark 的默认作业结果已足以证明路径与相对量级,但 `Initialization_*` 与少量 short-run 结果仍不应直接当成稳定排序结论。
## 最近权威验证
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
+- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
+ - 结果:通过,`Passed: 4, Failed: 0`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix pr347-req-scoped --filter "*RequestLifetimeBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:`Singleton 52.69 ns / 32 B`、`Transient 57.88 ns / 56 B`、`Scoped 144.72 ns / 368 B`
@@ -72,9 +109,9 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 再次运行 `$gframework-pr-review` 复核 `PR #347` latest-head open thread 是否已随本轮 head 收敛。
-2. 若 review 已清空,继续留在 `GFramework.Cqrs.Benchmarks` 单模块推进下一批 benchmark 对照,而不是立即扩散到 runtime。
-3. 若 review 仍保留 benchmark 相关线程,优先区分 stale 与新增结论,再决定是否需要新的 scoped-host 或 artifacts 入口修补。
+1. 先提交当前未提交的 `NotificationLifetime + registration fallback tests + CQRS/legacy docs` 收尾切片,回收工作树到干净状态。
+2. 再次运行 `$gframework-pr-review`,复核 `PR #347` latest-head open thread 是否已随着本轮多波 head 收敛。
+3. 若继续扩 benchmark,优先从 `GFramework.Cqrs.Benchmarks/README.md` 已明确列出的 gap 中选下一个单文件切片,而不是继续扩大 shared infra 改动面。
## 活跃文档
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 d399a86c..444ffc87 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
@@ -64,3 +64,55 @@ SPDX-License-Identifier: Apache-2.0
### 当前下一步
- 推送本轮变更后,重新运行 `$gframework-pr-review`,确认 `PR #347` 的 latest-head open thread 是否已随着新 head 收敛。
+
+### 阶段:多波 batch 收口与 benchmark / docs 扩面(CQRS-REWRITE-RP-133)
+
+- 按 `$gframework-batch-boot` 启动多波 non-conflicting subagent,基线固定为
+ `origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`。
+- 启动前分支累计 diff 为 `0 files / 0 lines`;自然停点时累计 branch diff 约为 `12 files`。
+- 主线程把 stop decision 明确交给 `reviewability / context-budget`,没有在仍有文件预算时继续机械追到 `50 files`。
+- 本轮 accepted delegated scope:
+ - runtime / tests
+ - `CqrsNotificationPublisherTests`:补“多 publisher 报错”与“publisher 缓存复用”回归
+ - `CqrsGeneratedRequestInvokerProviderTests` + `CqrsHandlerRegistrar`:补 generated descriptor 坏元数据、异常枚举、重复 pair 回退契约
+ - `CqrsDispatcherCacheTests`:补 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
+ - `CqrsRegistrationServiceTests`:补稳定程序集键 fallback 到 `AssemblyName.Name` / `ToString()` 的回归
+ - benchmarks
+ - `RequestStartupBenchmarks`:补 `Mediator` startup 对照
+ - `StreamStartupBenchmarks`
+ - `NotificationStartupBenchmarks`
+ - `NotificationLifetimeBenchmarks`
+ - `GFramework.Cqrs.Benchmarks/README.md`:收口当前 coverage / gap / smoke 解释边界
+ - docs / recovery
+ - `GFramework.Cqrs/README.md`
+ - `docs/zh-CN/core/cqrs.md`
+ - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
+ - `docs/zh-CN/core/command.md`
+ - `docs/zh-CN/core/query.md`
+ - `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix notif-lifetime --filter "*NotificationLifetimeBenchmarks*"`
+- 本轮 benchmark 结果摘要:
+ - `RequestStartupBenchmarks`
+ - `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
+ - `ColdStart_Mediator 110.867 us / 57872 B`
+ - `ColdStart_MediatR 679.103 us / 606256 B`
+ - `StreamStartupBenchmarks`
+ - `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
+ - `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
+ - `ColdStart_MediatR 933.87 us / 678992 B`
+ - `NotificationStartupBenchmarks`
+ - `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
+ - `ColdStart_Mediator 136.08 us / 62512 B`
+ - `ColdStart_MediatR 1.379 ms / 719056 B`
+ - `NotificationLifetimeBenchmarks`
+ - `Singleton`:`GFramework 295.48 ns / 360 B`,`MediatR 77.99 ns / 288 B`
+ - `Scoped`:`GFramework 410.92 ns / 640 B`,`MediatR 213.49 ns / 632 B`
+ - `Transient`:`GFramework 311.21 ns / 416 B`,`MediatR 74.36 ns / 288 B`
+- 当前收尾判断:
+ - branch diff 仍远低于 `50` 文件阈值,但 active 未提交面与 benchmark 运行输出已经足够构成自然 stop boundary
+ - 下一步不继续扩 batch,先提交当前收尾切片并回到干净工作树,再按 PR review 结果决定后续波次
diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md
index 25355a72..c673d62a 100644
--- a/docs/zh-CN/core/command.md
+++ b/docs/zh-CN/core/command.md
@@ -109,25 +109,45 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline,也不会额外补上下文桥接链路。
+## 兼容入口和 CQRS bridge 的关系
+
+这里可以把旧命令路径理解成“保留旧 API、内部接到新 runtime”:
+
+- 对调用方来说,`SendCommand(...)` / `SendCommandAsync(...)` 仍然是旧命令入口
+- 对运行时来说,标准 `Architecture` 路径会把这些旧命令包装成内部 bridge request,再交给 `ICqrsRuntime`
+- 对处理过程来说,命令最终会复用当前 CQRS 的 request pipeline 与上下文注入链路,而不是维持一套完全独立的分发栈
+
+因此,兼容入口的意义主要是降低迁移成本,而不是鼓励新模块继续围绕旧执行器设计。
+
在 `IContextAware` 对象内,通常直接通过扩展使用:
```csharp
using GFramework.Core.Extensions;
```
-## 什么时候还应该用旧命令
+## 什么时候继续保留旧命令
- 你在维护既有 `Core.Command` 代码
- 你的调用链已经依赖旧 `CommandExecutor`
- 当前改动目标是局部修复,不值得同时做 CQRS 迁移
+- 你需要保持现有命令类型、调用入口或测试夹具不变,只希望它们在标准架构下继续工作
-## 什么时候该切到 CQRS
+这类场景的重点是“让存量代码继续跑”,而不是把旧命令体系当成新模块默认入口。
-下面这些场景更适合新 CQRS runtime:
+## 什么时候该开始迁移
+
+如果出现下面这些信号,说明更适合把命令迁到新 CQRS:
- 需要 request / notification / stream 的统一模型
- 需要 pipeline behaviors
- 需要 handler registry 生成器
- 你正在写新的业务模块,而不是维护历史命令代码
+- 你希望命令处理逻辑直接落在 `AbstractCommandHandler<,>` 等 CQRS handler 上,而不是继续扩展 `AbstractCommand*`
+- 你需要让命令和查询、通知共用同一套注册与调试路径
+
+一个简单判断方法:
+
+- 继续保留旧路径:为了兼容已有 `Command` 类型和调用链
+- 迁移到 CQRS:为了给新功能建立统一 request model,而不是继续扩大 legacy 面积
迁移后常见写法见:[cqrs](./cqrs.md)
diff --git a/docs/zh-CN/core/query.md b/docs/zh-CN/core/query.md
index e01cde2f..97eaa0e4 100644
--- a/docs/zh-CN/core/query.md
+++ b/docs/zh-CN/core/query.md
@@ -85,6 +85,17 @@ var count = this.SendQuery(
在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`。
因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。
+只有在你直接 `new QueryExecutor()` 或 `new AsyncQueryExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;这时异步查询也不会进入统一 CQRS pipeline。
+
+## 兼容入口和 CQRS bridge 的关系
+
+旧查询页面的重点不是再引入一套新执行模型,而是说明兼容入口现在如何接到 CQRS runtime:
+
+- `SendQuery(...)` / `SendQueryAsync(...)` 仍然是面向存量代码的旧 API
+- 标准 `Architecture` 路径会把旧查询包装成内部 bridge request,再交给 `ICqrsRuntime`
+- 这让旧查询对象在不改调用方式的前提下,也能共享当前 CQRS 的 pipeline、handler 调度和上下文注入语义
+
+如果你依赖的是 direct executor 测试或隔离运行,那么仍要把它看成 legacy 路径,而不是完整的新 CQRS 使用方式。
在 `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
@@ -97,10 +108,11 @@ using GFramework.Core.Extensions;
- 你在维护现有 `Core.Query` 代码
- 当前代码已经建立在旧查询执行器之上
- 你只想修正局部行为,不想顺手迁移整条调用链
+- 你需要保留现有 `AbstractQuery*` 类型与测试入口,只要求标准架构下继续复用统一 runtime
## 什么时候改用 CQRS 查询
-如果你正在写新的读取路径,优先考虑:
+如果你正在写新的读取路径,或者已经需要统一读写模型,优先考虑:
- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery`
- `AbstractQueryHandler`
@@ -108,4 +120,9 @@ using GFramework.Core.Extensions;
原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。
+可以按下面的判断来选:
+
+- 继续保留旧路径:为了兼容已有 `Query` 类型、旧执行器或局部修复场景
+- 迁移到 CQRS:为了把新的读取能力纳入统一 request model,而不是继续扩大 legacy 查询面
+
继续阅读:[cqrs](./cqrs.md)