diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
index 57d46d23..61f3a13d 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -218,6 +219,59 @@ internal static class BenchmarkHostFactory
return await mediator.Send(request, cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// 在真实的 request 级作用域内创建一次 GFramework.CQRS stream,并让该作用域覆盖整个异步枚举周期。
+ ///
+ /// stream 响应元素类型。
+ /// 冻结后的 benchmark 根容器,用于创建 request 作用域并提供注册元数据。
+ /// 当前 request 级 runtime 复用的日志器。
+ /// 当前 CQRS 分发上下文。
+ /// 要创建 stream 的 request。
+ /// 取消令牌。
+ /// 绑定到单次显式作用域的异步响应序列。
+ ///
+ /// stream 与 request 的区别在于:handler 解析发生在建流时,但 scoped 依赖必须一直存活到枚举完成。
+ /// 因此这里返回一个包装后的 async iterator,把 scope 的释放时机推迟到调用方结束枚举之后,
+ /// 避免 `Scoped` handler 退化成“建流后立刻释放 scope,再在根容器语义下继续枚举”的错误模型。
+ ///
+ internal static IAsyncEnumerable CreateScopedGFrameworkStream(
+ MicrosoftDiContainer rootContainer,
+ ILogger runtimeLogger,
+ ICqrsContext context,
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(rootContainer);
+ ArgumentNullException.ThrowIfNull(runtimeLogger);
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(request);
+
+ return EnumerateScopedGFrameworkStreamAsync(rootContainer, runtimeLogger, context, request, cancellationToken);
+ }
+
+ ///
+ /// 在真实的 request 级作用域内创建一次 MediatR stream,并让该作用域覆盖整个异步枚举周期。
+ ///
+ /// stream 响应元素类型。
+ /// 当前 benchmark 的根 。
+ /// 要创建 stream 的 request。
+ /// 取消令牌。
+ /// 绑定到单次显式作用域的异步响应序列。
+ ///
+ /// 这里与 scoped request helper 保持同一组边界约束,但把 scope 生命周期延长到 stream 完整枚举结束,
+ /// 确保 `Scoped` handler 与依赖不会在首个元素产出前后被提前释放。
+ ///
+ internal static IAsyncEnumerable CreateScopedMediatRStream(
+ ServiceProvider rootServiceProvider,
+ MediatR.IStreamRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(rootServiceProvider);
+ ArgumentNullException.ThrowIfNull(request);
+
+ return EnumerateScopedMediatRStreamAsync(rootServiceProvider, request, cancellationToken);
+ }
+
///
/// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。
///
@@ -251,4 +305,47 @@ internal static class BenchmarkHostFactory
interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == openGenericContract);
}
+
+ ///
+ /// 在单个显式作用域内创建并枚举 GFramework.CQRS stream。
+ ///
+ private static async IAsyncEnumerable EnumerateScopedGFrameworkStreamAsync(
+ MicrosoftDiContainer rootContainer,
+ ILogger runtimeLogger,
+ ICqrsContext context,
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest request,
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ using var scope = rootContainer.CreateScope();
+ var scopedContainer = new ScopedBenchmarkContainer(rootContainer, scope);
+ var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
+ scopedContainer,
+ runtimeLogger);
+ var stream = runtime.CreateStream(context, request, cancellationToken);
+
+ await foreach (var response in stream.ConfigureAwait(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return response;
+ }
+ }
+
+ ///
+ /// 在单个显式作用域内创建并枚举 MediatR stream。
+ ///
+ private static async IAsyncEnumerable EnumerateScopedMediatRStreamAsync(
+ ServiceProvider rootServiceProvider,
+ MediatR.IStreamRequest request,
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ using var scope = rootServiceProvider.CreateScope();
+ var mediator = scope.ServiceProvider.GetRequiredService();
+ var stream = mediator.CreateStream(request, cancellationToken);
+
+ await foreach (var response in stream.ConfigureAwait(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return response;
+ }
+ }
}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
index 286af43e..3f1c1738 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
@@ -25,9 +25,9 @@ namespace GFramework.Cqrs.Benchmarks.Messaging;
/// 对比 stream 在不同 handler 生命周期与观测方式下的额外开销。
///
///
-/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
-/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起,
-/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。
+/// 当前矩阵覆盖 `Singleton`、`Scoped` 与 `Transient`。
+/// 其中 `Scoped` 会在每次建流与枚举期间显式创建并持有真实的 DI 作用域,
+/// 避免把 scoped handler 错误地下沉到根容器解析,或在异步枚举尚未结束时提前释放作用域。
/// 当前只保留 与
/// 两种模式,分别用于观察建流到首个元素的固定成本与完整枚举的总成本,
/// 以避免把更多观测策略与 的生命周期对照目标混在一起。
@@ -45,11 +45,13 @@ public class StreamLifetimeBenchmarks
private ReflectionBenchmarkStreamRequest _reflectionRequest = null!;
private GeneratedBenchmarkStreamRequest _generatedRequest = null!;
private MediatRBenchmarkStreamRequest _mediatrRequest = null!;
+ private ILogger _reflectionRuntimeLogger = null!;
+ private ILogger _generatedRuntimeLogger = null!;
///
/// 控制当前 benchmark 使用的 handler 生命周期。
///
- [Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
+ [Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
public HandlerLifetime Lifetime { get; set; }
///
@@ -68,6 +70,11 @@ public class StreamLifetimeBenchmarks
///
Singleton,
+ ///
+ /// 每次建流在显式作用域内解析并复用 handler 实例,且作用域会覆盖整个枚举周期。
+ ///
+ Scoped,
+
///
/// 每次建流都重新解析新的 handler 实例。
///
@@ -122,14 +129,21 @@ public class StreamLifetimeBenchmarks
_reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3);
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
+ _reflectionRuntimeLogger =
+ LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime);
+ _generatedRuntimeLogger =
+ LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime);
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
RegisterReflectionHandler(container, Lifetime);
});
- _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
- _reflectionContainer,
- LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime));
+ if (Lifetime != HandlerLifetime.Scoped)
+ {
+ _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
+ _reflectionContainer,
+ _reflectionRuntimeLogger);
+ }
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
@@ -138,16 +152,22 @@ public class StreamLifetimeBenchmarks
});
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
// 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
- _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
- _generatedContainer,
- LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime));
+ if (Lifetime != HandlerLifetime.Scoped)
+ {
+ _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
+ _generatedContainer,
+ _generatedRuntimeLogger);
+ }
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(StreamLifetimeBenchmarks),
static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler),
ResolveMediatRLifetime(Lifetime));
- _mediatr = _serviceProvider.GetRequiredService();
+ if (Lifetime != HandlerLifetime.Scoped)
+ {
+ _mediatr = _serviceProvider.GetRequiredService();
+ }
}
///
@@ -181,6 +201,18 @@ public class StreamLifetimeBenchmarks
[Benchmark]
public ValueTask Stream_GFrameworkReflection()
{
+ if (Lifetime == HandlerLifetime.Scoped)
+ {
+ return ObserveAsync(
+ BenchmarkHostFactory.CreateScopedGFrameworkStream(
+ _reflectionContainer,
+ _reflectionRuntimeLogger,
+ BenchmarkContext.Instance,
+ _reflectionRequest,
+ CancellationToken.None),
+ Observation);
+ }
+
return ObserveAsync(
_reflectionRuntime.CreateStream(
BenchmarkContext.Instance,
@@ -195,6 +227,18 @@ public class StreamLifetimeBenchmarks
[Benchmark]
public ValueTask Stream_GFrameworkGenerated()
{
+ if (Lifetime == HandlerLifetime.Scoped)
+ {
+ return ObserveAsync(
+ BenchmarkHostFactory.CreateScopedGFrameworkStream(
+ _generatedContainer,
+ _generatedRuntimeLogger,
+ BenchmarkContext.Instance,
+ _generatedRequest,
+ CancellationToken.None),
+ Observation);
+ }
+
return ObserveAsync(
_generatedRuntime.CreateStream(
BenchmarkContext.Instance,
@@ -209,6 +253,16 @@ public class StreamLifetimeBenchmarks
[Benchmark]
public ValueTask Stream_MediatR()
{
+ if (Lifetime == HandlerLifetime.Scoped)
+ {
+ return ObserveAsync(
+ BenchmarkHostFactory.CreateScopedMediatRStream(
+ _serviceProvider,
+ _mediatrRequest,
+ CancellationToken.None),
+ Observation);
+ }
+
return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation);
}
@@ -229,6 +283,12 @@ public class StreamLifetimeBenchmarks
ReflectionBenchmarkStreamHandler>();
return;
+ case HandlerLifetime.Scoped:
+ container.RegisterScoped<
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
+ ReflectionBenchmarkStreamHandler>();
+ return;
+
case HandlerLifetime.Transient:
container.RegisterTransient<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
@@ -261,6 +321,12 @@ public class StreamLifetimeBenchmarks
GeneratedBenchmarkStreamHandler>();
return;
+ case HandlerLifetime.Scoped:
+ container.RegisterScoped<
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
+ GeneratedBenchmarkStreamHandler>();
+ return;
+
case HandlerLifetime.Transient:
container.RegisterTransient<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
@@ -282,6 +348,7 @@ public class StreamLifetimeBenchmarks
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.")
};
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index ba189b58..51dc8155 100644
--- a/GFramework.Cqrs.Benchmarks/README.md
+++ b/GFramework.Cqrs.Benchmarks/README.md
@@ -17,9 +17,9 @@
- `Messaging/RequestBenchmarks.cs`
- direct handler、NuGet `Mediator` source-generated concrete path、已接上 handwritten generated request invoker provider 的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestLifetimeBenchmarks.cs`
- - `Singleton / Transient` 两类 handler 生命周期下,direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
+ - `Singleton / Scoped / Transient` 三类 handler 生命周期下,direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比;其中 `Scoped` 通过真实 request 级作用域宿主执行,不再把 scoped handler 退化为根容器解析
- `Messaging/StreamLifetimeBenchmarks.cs`
- - `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举分层对照
+ - `Singleton / Scoped / Transient` 三类 handler 生命周期下,direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 分层对照,并同时提供 `FirstItem / DrainAll` 两种观测口径
- `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下,direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestStartupBenchmarks.cs`
@@ -27,13 +27,13 @@
- `Messaging/RequestInvokerBenchmarks.cs`
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/StreamInvokerBenchmarks.cs`
- - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 完整枚举对比
+ - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
- `Messaging/NotificationBenchmarks.cs`
- `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比
- `Messaging/NotificationFanOutBenchmarks.cs`
- fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比
- `Messaging/StreamingBenchmarks.cs`
- - direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比
+ - direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
## 最小使用方式
@@ -60,8 +60,10 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容
- `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径
+- `RequestLifetimeBenchmarks` 的 `Scoped` 场景会在每次 request 分发时显式创建并释放真实 DI 作用域,用来观察 scoped handler 绑定到 request 边界后的解析与 dispatch 成本
- `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem` 与 `DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察
-- 当前短跑结果显示,`StreamLifetimeBenchmarks` 在 `Singleton` 下无论 `FirstItem` 还是 `DrainAll` 都表现为 generated 略优于 reflection;在 `Transient` 下,`FirstItem` 仍是 reflection 略优于 generated,但 `DrainAll` 已转为 generated 优于 reflection。这说明当前差值主要集中在建流到首个元素之间的瞬时成本,而不是完整枚举阶段整体退化
+- `StreamingBenchmarks` 与 `StreamInvokerBenchmarks` 都同时暴露 `FirstItem` 与 `DrainAll`;阅读结果时应把它们分别理解为“建流到首个元素”的固定成本观测与“完整枚举整个 stream”的总成本观测
+- `StreamInvokerBenchmarks` 当前的 `DrainAll` short-job 输出只适合做 smoke 复核,确认矩阵和路径可以正常跑通;它不应直接写成 reflection、generated 或 `MediatR` 之间的稳定性能结论,若要做排序判断,应复跑默认作业或更完整的 benchmark 批次
- 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主,就应至少复跑能覆盖该路径的过滤场景;request 热路径通常先看:
- `RequestBenchmarks.SendRequest_*`
- `RequestLifetimeBenchmarks.SendRequest_*`
@@ -75,5 +77,4 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- 若继续优化 stream lifetime,可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
-- 带真实显式作用域边界的 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 58ff9be8..61544c29 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,21 +7,23 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-129`
+- 恢复点编号:`CQRS-REWRITE-RP-130`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`待重新抓取`
- 当前结论:
-- 当前 `RP-129` 继续沿用 `$gframework-batch-boot 50`,但本轮按 `gframework-multi-agent-batch` 的职责边界组织了一波 `3` 路互不冲突的 benchmark worker:`StreamingBenchmarks` 观测口径拆分、`StreamInvokerBenchmarks` 观测口径拆分、`RequestLifetimeBenchmarks` 的真实 scoped-host 生命周期矩阵
-- 本轮启动前已重新按 skill 规则复核基线:`origin/main` 与本地 `main` 当前都在 `699d0b48`(`2026-05-09 18:39:38 +0800`),且启动时 `origin/main...HEAD` 的累计 branch diff 为 `0 files / 0 lines`;旧 active 入口里 `21 files` 的数字已不再可作为当前波次基线
-- 本轮写面限定在 benchmark 子系统:`GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs`、`StreamInvokerBenchmarks.cs`、`RequestLifetimeBenchmarks.cs`、`BenchmarkHostFactory.cs`,以及新增的 `ScopedBenchmarkContainer.cs`;没有扩散到 `GFramework.Cqrs` runtime、测试项目或公共文档
-- `StreamingBenchmarks` 已从单一完整枚举口径扩成 `FirstItem / DrainAll` 双观测模式;worker smoke 结果表明默认 generated-provider steady-state stream 宿主在两种口径下都能稳定跑通,当前 short-job 约为 `FirstItem: baseline 62.14 ns / GFramework 118.83 ns / MediatR 182.16 ns`,`DrainAll: baseline 94.34 ns / GFramework 149.57 ns / MediatR 280.77 ns`
-- `StreamInvokerBenchmarks` 现也具备 `FirstItem / DrainAll` 双观测模式,并已完成串行 smoke 运行;当前 short-job 下,`FirstItem` 口径里 generated lane 约 `59.44 ns`、reflection 约 `52.90 ns`,说明 tracking 里提到的 “generated 在首项前略慢于 reflection” 信号在更窄的 invoker 场景里仍可见
-- `RequestLifetimeBenchmarks` 当前矩阵已从 `Singleton / Transient` 扩到 `Singleton / Scoped / Transient`,且 `Scoped` 不再退化为根容器解析,而是通过 `BenchmarkHostFactory` + `ScopedBenchmarkContainer` 在每次 request 分发时显式创建并释放真实作用域;本轮 short-job 下 `Scoped` 口径约为 baseline `5.60 ns / 32 B`、`MediatR` `170.94 ns / 648 B`、`GFramework.Cqrs` `575.92 ns / 3400 B`
-- 本轮首次并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时触发自动生成目录争用;已按仓库规则改为串行重跑同一命令,并以串行结果作为权威 smoke 验证
-- 当前可恢复结论收口为两点:一是 stream benchmark 线已从 `StreamLifetimeBenchmarks` 继续下探到 default steady-state 与 invoker 两个更窄口径;二是 request lifetime 线已经拥有真实 scoped-host 基线,后续若继续扩 `StreamLifetimeBenchmarks` 的 scoped 口径,不必再先做宿主适配
+- 当前 `RP-130` 延续 `$gframework-batch-boot 50`,并在上一波 `StreamingBenchmarks`、`StreamInvokerBenchmarks`、`RequestLifetimeBenchmarks` 扩口径之后,继续把 `StreamLifetimeBenchmarks` 的生命周期矩阵补齐到真实 `Scoped` stream 作用域,同时把 benchmark `README` 与当前矩阵同步
+- 本轮继续沿用已复核的基线:`origin/main` 与本地 `main` 当前都在 `699d0b48`(`2026-05-09 18:39:38 +0800`);当前分支连同本轮变更相对 `origin/main` 的累计 branch diff 约为 `9 files changed, 953 insertions(+), 83 deletions(-)`,离 `$gframework-batch-boot 50` 还有明显余量
+- 本轮写面收敛在 benchmark 层三处:`GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`、`StreamLifetimeBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/README.md`;没有扩散到 `GFramework.Cqrs` runtime 或测试项目
+- `BenchmarkHostFactory` 现在提供 `CreateScopedGFrameworkStream(...)` 与 `CreateScopedMediatRStream(...)`,通过显式 scope 包裹整个 async stream 枚举周期,避免 scoped handler 在建流后提前释放或错误回落到根容器语义
+- `StreamLifetimeBenchmarks` 当前矩阵已完整覆盖 `Singleton / Scoped / Transient` 与 `FirstItem / DrainAll`:
+ - `Singleton` 下 `DrainAll` 仍表现为 generated 略优于 reflection,约 `131.96 ns` 对 `134.26 ns`
+ - `Scoped` 下已经可以稳定产出真实作用域矩阵;`FirstItem` 约为 baseline `56.24 ns`、`MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns`,`DrainAll` 约为 baseline `81.20 ns`、`MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns`
+ - `Transient` 下 `FirstItem` 约为 generated `107.79 ns`、reflection `112.60 ns`,但 `DrainAll` 仍是 reflection `131.41 ns` 略优于 generated `135.95 ns`
+- `README` 已同步三类 benchmark 现状:`RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 都包含真实 `Scoped` 生命周期,`StreamingBenchmarks` / `StreamInvokerBenchmarks` 都显式区分 `FirstItem / DrainAll`,且 `StreamInvokerBenchmarks` 的 `DrainAll` short-job 输出被明确标注为 smoke-only,不作为稳定排序结论
+- 本轮再次确认:此前并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时出现的冲突属于 benchmark 工件/生成目录层面的运行隔离问题,而不是 `Fixture`、`StreamInvokerBenchmarks` 或 `StreamLifetimeBenchmarks` 的业务逻辑错误
- 下一推荐步骤:
- - 先回到 `ai-plan/public/cqrs-rewrite/**` 与 `GFramework.Cqrs.Benchmarks/README.md`,把本轮基线从旧的 `21 files / PR #345` 状态更新到当前 `699d0b48` 基线和新的 benchmark 结论
- - 然后优先复核 `StreamInvokerBenchmarks` 当前 short-job 输出里 `DrainAll` 口径的异常排序是否只是 smoke 配置噪音,必要时把下一批切回更稳定的 benchmark job 或补更窄的 helper-level 对照,而不是直接据此下结论
+- 下一批优先切到 `GFramework.Cqrs.Benchmarks` 的 benchmark-run/config 隔离层,避免两个过滤 benchmark 并行执行时再次共享自动生成目录或 artifacts 路径
+- 完成运行隔离后,再决定是否单开一波更稳定的 `StreamInvokerBenchmarks` `DrainAll` 复核,或继续把 scoped host 对照扩展到更多 stream 子场景
- 更早的 `RP-123` 及之前阶段细节以下方 trace 与归档为准,active 入口不再重复展开旧阶段流水。
- 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界
- 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略
@@ -157,6 +159,17 @@ CQRS 迁移与收敛。
## 最近权威验证
+- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:覆盖 `BenchmarkHostFactory` scoped stream helper、`StreamLifetimeBenchmarks` scoped 生命周期矩阵与 `README` 同步后的最小 Release build
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:`StreamLifetimeBenchmarks` 已稳定跑出 `24` 个 case;`Scoped + DrainAll` 当前约为 baseline `81.20 ns / 280 B`、`MediatR` `428.66 ns / 1224 B`、generated `692.05 ns / 3888 B`、reflection `716.61 ns / 3888 B`
+- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.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`
+ - 结果:通过
+- `git --git-dir=/.git/worktrees/GFramework-cqrs --work-tree=. diff --check`
+ - 结果:通过
+
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:覆盖 `StreamLifetimeBenchmarks` 的代码收口与 benchmark `README` 同步后的最小 Release build
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 7a12cbaf..b48a495c 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,31 @@
## 2026-05-11
+### 阶段:stream lifetime scoped 矩阵与 README 同步(CQRS-REWRITE-RP-130)
+
+- 延续 `$gframework-batch-boot 50`,在上一波把 `StreamingBenchmarks`、`StreamInvokerBenchmarks` 与 `RequestLifetimeBenchmarks` 扩成双观测 / scoped-request 基线后,本轮先不回到 runtime,而是只补齐 `StreamLifetimeBenchmarks` 的真实 `Scoped` stream 生命周期矩阵,并同步 benchmark `README`
+- 本轮主线程验收与修正:
+ - 接受 worker 留在 `BenchmarkHostFactory.cs` 的 scoped stream helper 方向,但把实现收口为“创建 scope -> 在 scope 内创建 runtime / mediator -> 让 scope 覆盖完整 async stream 枚举周期”的包装迭代器,而不是只在建流阶段短暂持有作用域
+ - `StreamLifetimeBenchmarks.cs` 现仅在 `Lifetime == Scoped` 时改走新的 scoped stream helper;`Singleton / Transient` 仍保持原有最小 steady-state 宿主,不把 scoped 宿主额外成本误混进其他矩阵
+ - `GFramework.Cqrs.Benchmarks/README.md` 已同步 `RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 的 `Scoped` 现状,并把 `StreamInvokerBenchmarks` `DrainAll` 标成 smoke-only 结论
+- 本轮验证:
+ - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.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`
+ - 结果:通过
+ - `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 --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:`24` 个 case 全部跑通;`Scoped + FirstItem` 约为 baseline `56.24 ns`、`MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns`;`Scoped + DrainAll` 约为 baseline `81.20 ns`、`MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns`
+- 本轮结论:
+ - `StreamLifetimeBenchmarks` 现在已经具备与 `RequestLifetimeBenchmarks` 对称的真实 scoped-host 作用域边界,后续不必再先修 benchmark 宿主才能观察 stream scoped lifetime
+ - `Scoped` 成本当前明显高于 `Singleton / Transient`,说明这条矩阵已经把“真实 scope 生命周期”本身的常量开销带入观测;这正是本轮想要确认的边界,而不是回归或异常
+ - 本轮并没有改变 `GFramework.Cqrs` runtime 或业务逻辑,只是把 benchmark 宿主语义与文档描述对齐到当前事实
+- 下一恢复点:
+ - 优先切到 benchmark 运行隔离层,只动 `GFramework.Cqrs.Benchmarks/Program.cs` 与必要的 `README` 说明,解决两个过滤 benchmark 并行运行时共享 auto-generated build/artifacts 目录的问题
+ - 新开的 explorer 已确认根因集中在 `Program.cs` 直接把 `args` 原样交给 `BenchmarkSwitcher.Run(args)`,当前没有为每次运行注入唯一 `ArtifactsPath`;不建议下一批回头改 `Fixture.cs` 或 benchmark 业务逻辑
+
### 阶段:benchmark 多 worker 波次与 scoped-host 基线(CQRS-REWRITE-RP-129)
- 本轮从 `$gframework-batch-boot 50` 启动,但按 `gframework-multi-agent-batch` 规则把非阻塞工作拆成三条互不冲突的 benchmark 切片: