diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 633a88af..499c6e54 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -252,6 +252,18 @@ public interface IIocContainer : IContextAware, IDisposable /// bool Contains() where T : class; + /// + /// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求解析出实例。 + /// + /// 要检查的服务类型。 + /// 若存在显式注册或开放泛型映射可满足该服务类型,则返回 ;否则返回 + /// + /// 该入口面向“先判断是否值得解析实例”的热路径优化场景。 + /// 与 不同,它不会为了判断结果而激活服务实例,因此可避免把瞬态对象创建、 + /// 多服务枚举或日志分配混入仅需存在性判断的调用链中。 + /// + bool HasRegistration(Type type); + /// /// 判断容器中是否包含某个具体的实例对象 /// diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 38832cdd..61db29ba 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -419,6 +419,32 @@ public class MicrosoftDiContainerTests Assert.That(_container.Contains(), Is.False); } + /// + /// 测试显式服务不存在时,HasRegistration 应返回 false,且不会要求先冻结或解析实例。 + /// + [Test] + public void HasRegistration_WithNoMatchingService_Should_ReturnFalse() + { + Assert.That(_container.HasRegistration(typeof(IPipelineBehavior)), Is.False); + } + + /// + /// 测试 HasRegistration 能识别开放泛型 CQRS pipeline 行为对闭合请求/响应对的可见性。 + /// + [Test] + public void HasRegistration_Should_ReturnTrue_For_Closed_Service_Satisfied_By_Open_Generic_Registration() + { + _container.GetServicesUnsafe.AddSingleton( + typeof(IPipelineBehavior<,>), + typeof(OpenGenericHasRegistrationBehavior<,>)); + + Assert.That(_container.HasRegistration(typeof(IPipelineBehavior)), Is.True); + + _container.Freeze(); + + Assert.That(_container.HasRegistration(typeof(IPipelineBehavior)), Is.True); + } + /// /// 测试当实例存在时检查实例包含关系应返回 true 的功能 /// @@ -902,4 +928,32 @@ public class MicrosoftDiContainerTests Assert.That(lockField, Is.Not.Null); return (ReaderWriterLockSlim)lockField!.GetValue(container)!; } + + /// + /// 供 HasRegistration 回归使用的最小请求类型。 + /// + private sealed class HasRegistrationRequest : IRequest + { + } + + /// + /// 供 HasRegistration 回归使用的开放泛型 pipeline 行为。 + /// + /// 请求类型。 + /// 响应类型。 + private sealed class OpenGenericHasRegistrationBehavior : + IPipelineBehavior + where TRequest : IRequest + { + /// + /// 透传到下一个 pipeline 节点,不额外改变请求语义。 + /// + public ValueTask Handle( + TRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(request, cancellationToken); + } + } } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 906d0b5d..4a4be8d4 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -706,9 +706,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } var result = _provider!.GetService(type); - _logger.Debug(result != null - ? $"Retrieved instance: {type.Name}" - : $"No instance found for type: {type.Name}"); + if (_logger.IsDebugEnabled()) + { + _logger.Debug(result != null + ? $"Retrieved instance: {type.Name}" + : $"No instance found for type: {type.Name}"); + } return result; } finally @@ -792,7 +795,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } var services = _provider!.GetServices().ToList(); - _logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}"); + if (_logger.IsDebugEnabled()) + { + _logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}"); + } return services; } finally @@ -821,7 +827,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } var services = _provider!.GetServices(type).ToList(); - _logger.Debug($"Retrieved {services.Count} instances of {type.Name}"); + if (_logger.IsDebugEnabled()) + { + _logger.Debug($"Retrieved {services.Count} instances of {type.Name}"); + } return services.Where(o => o != null).Cast().ToList(); } finally @@ -1023,6 +1032,26 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求先解析实例。 + /// + /// 要检查的服务类型。 + /// 若存在显式注册或开放泛型映射可满足该服务类型,则返回 ;否则返回 + public bool HasRegistration(Type type) + { + ArgumentNullException.ThrowIfNull(type); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); + try + { + return HasRegistrationCore(type); + } + finally + { + _lock.ExitReadLock(); + } + } + /// /// 判断容器中是否包含某个具体的实例对象 /// 通过已注册实例集合进行快速查找 @@ -1043,6 +1072,50 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 在当前容器状态下检查指定服务类型是否存在可见注册。 + /// + /// 要检查的服务类型。 + /// 存在可满足该类型的注册时返回 ;否则返回 + /// + /// 该检查只回答“是否可能解析到服务”,不会为了判断结果而激活实例。 + /// 预冻结阶段只基于当前服务描述符推断;冻结后则同样只观察描述符, + /// 避免把瞬态/多实例解析成本混入热路径中的存在性判断。 + /// + private bool HasRegistrationCore(Type requestedType) + { + foreach (var descriptor in GetServicesUnsafe) + { + if (CanSatisfyServiceType(descriptor.ServiceType, requestedType)) + { + return true; + } + } + + return false; + } + + /// + /// 判断某个服务描述符声明的服务类型是否能满足当前请求类型。 + /// + /// 注册时声明的服务类型。 + /// 调用方请求的服务类型。 + /// 若当前注册可用于解析 ,则返回 + private static bool CanSatisfyServiceType(Type registeredServiceType, Type requestedType) + { + if (registeredServiceType == requestedType || requestedType.IsAssignableFrom(registeredServiceType)) + { + return true; + } + + if (requestedType.IsConstructedGenericType && registeredServiceType.IsGenericTypeDefinition) + { + return requestedType.GetGenericTypeDefinition() == registeredServiceType; + } + + return false; + } + /// /// 清空容器中的所有实例和服务注册 /// 只有在容器未冻结状态下才能执行清空操作 diff --git a/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj index cbcdfb25..a93ae485 100644 --- a/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj +++ b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj @@ -18,6 +18,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs index b0248eb6..dde8acbe 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs @@ -75,6 +75,24 @@ internal static class BenchmarkHostFactory return services.BuildServiceProvider(); } + /// + /// 创建承载 `ai-libs/Mediator` source-generated concrete mediator 的最小对照宿主。 + /// + /// 补充当前场景的显式服务注册。 + /// 可直接解析 generated `Mediator.Mediator` 的 DI 宿主。 + /// + /// 当前 benchmark 只把 `Mediator` 作为单例 steady-state 对照组接入, + /// 因为它的 lifetime 由 source generator 在编译期塑形;若后续需要 `Transient` / `Scoped` 矩阵, + /// 应按 `ai-libs/Mediator/benchmarks` 的做法拆成独立 build config,而不是在同一编译产物里混用多个 lifetime。 + /// + internal static ServiceProvider CreateMediatorServiceProvider(Action? configure) + { + var services = new ServiceCollection(); + configure?.Invoke(services); + services.AddMediator(); + return services.BuildServiceProvider(); + } + /// /// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。 /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 25c6af14..af20fcf1 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -16,19 +16,22 @@ using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; using MediatR; using Microsoft.Extensions.DependencyInjection; +using GeneratedMediator = Mediator.Mediator; namespace GFramework.Cqrs.Benchmarks.Messaging; /// -/// 对比单个 request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的 steady-state dispatch 开销。 +/// 对比单个 request 在直接调用、GFramework.CQRS runtime、`ai-libs/Mediator` 与 MediatR 之间的 steady-state dispatch 开销。 /// [Config(typeof(Config))] public class RequestBenchmarks { private MicrosoftDiContainer _container = null!; private ICqrsRuntime _runtime = null!; - private ServiceProvider _serviceProvider = null!; + private ServiceProvider _mediatrServiceProvider = null!; + private ServiceProvider _mediatorServiceProvider = null!; private IMediator _mediatr = null!; + private GeneratedMediator _mediator = null!; private BenchmarkRequestHandler _baselineHandler = null!; private BenchmarkRequest _request = null!; @@ -69,23 +72,26 @@ public class RequestBenchmarks _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks))); - _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + _mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( configure: null, typeof(RequestBenchmarks), static candidateType => candidateType == typeof(BenchmarkRequestHandler), ServiceLifetime.Singleton); - _mediatr = _serviceProvider.GetRequiredService(); + _mediatr = _mediatrServiceProvider.GetRequiredService(); + + _mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null); + _mediator = _mediatorServiceProvider.GetRequiredService(); _request = new BenchmarkRequest(Guid.NewGuid()); } /// - /// 释放 MediatR 对照组使用的 DI 宿主。 + /// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。 /// [GlobalCleanup] public void Cleanup() { - BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider); } /// @@ -115,12 +121,22 @@ public class RequestBenchmarks return _mediatr.Send(_request, CancellationToken.None); } + /// + /// 通过 `ai-libs/Mediator` 的 source-generated concrete mediator 发送 request,作为高性能对照组。 + /// + [Benchmark] + public ValueTask SendRequest_Mediator() + { + return _mediator.Send(_request, CancellationToken.None); + } + /// /// Benchmark request。 /// /// 请求标识。 public sealed record BenchmarkRequest(Guid Id) : GFramework.Cqrs.Abstractions.Cqrs.IRequest, + Mediator.IRequest, MediatR.IRequest; /// @@ -134,6 +150,7 @@ public class RequestBenchmarks /// public sealed class BenchmarkRequestHandler : GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler, + Mediator.IRequestHandler, MediatR.IRequestHandler { /// diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 51682881..389f4e86 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -15,7 +15,7 @@ - `Messaging/Fixture.cs` - 运行前输出并校验场景配置 - `Messaging/RequestBenchmarks.cs` - - direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 + - direct handler、`GFramework.Cqrs` runtime、`ai-libs/Mediator` source-generated concrete path 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestLifetimeBenchmarks.cs` - `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestPipelineBenchmarks.cs` @@ -42,6 +42,7 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro ## 后续扩展方向 - request / stream 的真实 source-generator 产物与 handwritten generated provider 对照 +- `ai-libs/Mediator` 的 transient / scoped compile-time lifetime 矩阵对照 - stream handler 生命周期矩阵 - 带真实显式作用域边界的 scoped host 对照 - generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景 diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 695012c2..a5ba1c80 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -120,13 +120,17 @@ internal sealed class CqrsDispatcher( $"No CQRS request handler registered for {requestType.FullName}."); PrepareHandler(handler, context); + if (!container.HasRegistration(dispatchBinding.BehaviorType)) + { + return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false); + } + var behaviors = container.GetAll(dispatchBinding.BehaviorType); foreach (var behavior in behaviors) + { PrepareHandler(behavior, context); - - if (behaviors.Count == 0) - return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false); + } return await dispatchBinding.GetPipelineExecutor(behaviors.Count) .Invoke(handler, behaviors, request, cancellationToken) @@ -159,13 +163,17 @@ internal sealed class CqrsDispatcher( $"No CQRS stream handler registered for {requestType.FullName}."); PrepareHandler(handler, context); + if (!container.HasRegistration(dispatchBinding.BehaviorType)) + { + return (IAsyncEnumerable)dispatchBinding.StreamInvoker(handler, request, cancellationToken); + } + var behaviors = container.GetAll(dispatchBinding.BehaviorType); foreach (var behavior in behaviors) + { PrepareHandler(behavior, context); - - if (behaviors.Count == 0) - return (IAsyncEnumerable)dispatchBinding.StreamInvoker(handler, request, cancellationToken); + } return (IAsyncEnumerable)dispatchBinding.GetPipelineExecutor(behaviors.Count) .Invoke(handler, behaviors, dispatchBinding.StreamInvoker, request, cancellationToken); 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 1b0cc5eb..166fd5b4 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-100` +- 恢复点编号:`CQRS-REWRITE-RP-101` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #339` - 当前结论: @@ -36,15 +36,22 @@ CQRS 迁移与收敛。 - 当前 `RP-098` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,并收口 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 过宽吞掉 `InvalidOperationException` 的真实运行时诊断退化问题;现在仅把“上下文尚未就绪”视为允许 fallback 的信号,并为 fallback / 异常冒泡分别补齐回归测试 - `RP-099` 已补齐 `GFramework.Cqrs` 的最小 stream pipeline seam:新增 `IStreamPipelineBehavior<,>` / `StreamMessageHandlerDelegate<,>`、`RegisterCqrsStreamPipelineBehavior()`、dispatcher 侧 stream pipeline executor 缓存与 generated stream invoker 兼容回归,以及 `Architecture` 公开注册入口与对应文档说明 - 当前 `RP-100` 已使用 `$gframework-pr-review` 复核 `PR #339` latest-head review:收口 `RegisterCqrsStreamPipelineBehavior()` 的异常契约文档、为 `StreamPipelineInvocation.GetContinuation(...)` 补齐并发 continuation 缓存说明、抽取 `MicrosoftDiContainer` 的 CQRS 行为注册公共逻辑,并顺手修复当前 branch diff 内 `ICqrsRequestInvokerProvider.cs` 的 XML 缩进格式问题 -- `ai-plan` active 入口现以 `RP-100` 为最新恢复锚点;`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 + - 当前 `RP-101` 已按用户新增 benchmark 诉求收口 request 热路径:为 `IIocContainer` 新增不激活实例的 `HasRegistration(Type)`、让 dispatcher 在 `0 pipeline` 场景下跳过空行为解析,并为 `MicrosoftDiContainer` 的热路径查询补齐 debug-level 守卫,避免无效日志字符串分配 + - 当前 `RP-101` 已把 `GFramework.Cqrs.Benchmarks` 的 `Mediator` 对照组收口为官方 NuGet 引用(`Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`),不再使用本地 `ai-libs/Mediator` project reference;`RequestBenchmarks` 现已新增 source-generated concrete `Mediator` 对照方法,并通过 `RequestLifetimeBenchmarks` 复核 hot path 收口后的新基线 +- `ai-plan` active 入口现以 `RP-101` 为最新恢复锚点;`PR #339`、`PR #334`、`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 为 `31 files changed, 1176 insertions(+), 15 deletions(-)`,仍明显低于 `$gframework-batch-boot 50` 的文件 / 行数阈值 +- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`5dc2dd25`, 2026-05-08 09:08:37 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线 +- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`;本轮待提交工作树包含 9 个跟踪文件修改,另有 `BenchmarkDotNet.Artifacts/` 生成输出未纳入提交,仍明显低于 `$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.Cqrs.Benchmarks` 当前以 NuGet 方式引用 `Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`;`ai-libs/Mediator` 只保留为本地源码/README 对照资料,不再参与 benchmark 项目编译 +- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:约 `5.969 ns / 32 B`、`6.242 ns / 32 B`、`53.818 ns / 232 B`、`85.504 ns / 32 B` +- 当前 request lifetime benchmark 已从旧坏值显著收敛:`Singleton` 下 `GFramework.Cqrs` 约 `84.066 ns / 32 B`(旧值 `301.731 ns / 440 B`),`Transient` 下约 `90.652 ns / 56 B`(旧值 `287.863 ns / 464 B`) +- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配 +- 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间 - `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime` - 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline - `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试 @@ -83,6 +90,8 @@ CQRS 迁移与收敛。 - 若后续新增 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 内首次解析的对照基线 +- 当前 `Mediator` 对照组仅先接入 steady-state request;若要把 `Transient` / `Scoped` 生命周期矩阵也纳入同一组对照,需要按 `Mediator` 官方 benchmark 的做法拆分 compile-time lifetime build config,而不是在同一编译产物里混用多个 lifetime +- `BenchmarkDotNet.Artifacts/` 是本轮本地运行生成的未跟踪输出;若后续需要提交新的基准报告,应先确认仓库是否要保留该批产物,而不是默认把生成目录纳入版本控制 - 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” - 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 - legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline;若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成” @@ -100,6 +109,14 @@ CQRS 迁移与收敛。 - 备注:共享脚本确认 actual package set 与预期 14 个发布包完全一致 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`51/51` passed +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:最新 steady-state request 对照约为 baseline `5.969 ns / 32 B`、`Mediator` `6.242 ns / 32 B`、`MediatR` `53.818 ns / 232 B`、`GFramework.Cqrs` `85.504 ns / 32 B` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`Singleton` 下 `GFramework.Cqrs` / `MediatR` 约 `84.066 ns / 32 B` vs `56.096 ns / 232 B`;`Transient` 下约 `90.652 ns / 56 B` vs `57.207 ns / 232 B` - `python3 scripts/license-header.py --check` - 结果:通过 - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` @@ -204,9 +221,9 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #339` stale review threads;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核 -2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `notification publisher` 策略或 facade 公开入口中选择一个独立切片推进 -3. 若后续要增强 stream observability,优先评估是否需要元素级 hook,而不是直接复用当前建流级 seam 承载更多语义 +1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 benchmark/`Mediator` 对齐,下一批建议把 `Mediator` 的 compile-time lifetime 矩阵扩展到 `RequestLifetimeBenchmarks`,避免只有 `MediatR` 参与生命周期对照 +2. 若继续压 request steady-state 开销,下一批优先评估 `GFramework.Cqrs` 默认 request 路径吸收 generated invoker provider 的可行性,而不是只在单独 benchmark 类里保留 handwritten generated 对照 +3. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #339` stale review threads;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核 ## 活跃文档 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 ca5bbe60..a4a75b1a 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,49 @@ # CQRS 重写迁移追踪 +## 2026-05-08 + +### 阶段:request 热路径 benchmark 收口与 NuGet `Mediator` 对照补齐(CQRS-REWRITE-RP-101) + +- 延续 `$gframework-batch-boot 50`,本轮先按 `origin/main` 复核 branch diff 基线: + - `origin/main` = `5dc2dd25`,提交时间 `2026-05-08 09:08:37 +0800` + - 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines` + - 当前工作树仅新增 9 个跟踪文件修改,另有 `BenchmarkDotNet.Artifacts/` 本地生成输出未纳入提交范围,仍明显低于 `$gframework-batch-boot 50` 的文件阈值 +- 用户新增的 benchmark 诉求有两部分: + - 解释 `BenchmarkDotNet.Artifacts/results` 里为什么 `GFramework.Cqrs` request 路径表现显著差于对照组 + - 把 `martinothamar/Mediator` 加入 benchmark 对照,但必须使用官方 NuGet 包,不允许接本地 `ai-libs/Mediator` project reference +- 本轮主线程先回到 runtime hot path 与 benchmark 宿主做最小成本排查,确认旧坏值的两个主要根因: + - `CqrsDispatcher.SendAsync(...)` / `CreateStream(...)` 在 `0 pipeline` 场景下仍无条件执行 `container.GetAll(dispatchBinding.BehaviorType)`,即使根本没有行为注册,也会多走一次容器解析与空集合分配 + - `MicrosoftDiContainer.Get(Type)` / `GetAll()` / `GetAll(Type)` 在 debug logging 关闭时仍会先构造日志字符串,导致 benchmark 默认 `Fatal` 级别下仍持续产生无效分配 +- 本轮主线程决策: + - 为 `IIocContainer` 新增不激活实例的 `HasRegistration(Type)`,并由 `MicrosoftDiContainer` 提供支持开放泛型匹配的非激活查询实现 + - 让 `CqrsDispatcher` 在 request / stream 的 `0 pipeline` 场景先走 `HasRegistration(...)` fast-path;没有行为注册时直接调用已准备好的 request / stream invoker,不再解析空行为列表 + - 为 `MicrosoftDiContainer` 的热路径查询补 `IsDebugEnabled()` 守卫,避免 benchmark 常态配置下的无效日志字符串构造 + - 在 benchmark 项目中通过 NuGet 接入 `Mediator.Abstractions` 与 `Mediator.SourceGenerator` `3.0.2`,并让 `RequestBenchmarks` 使用 source-generated concrete `Mediator.Mediator` 作为新对照组 + - 保持 `ai-libs/Mediator` 只作为本地源码 / README 参考资料,不参与编译或项目引用 +- 本轮新增 / 更新的验证与回归覆盖: + - `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 新增 `HasRegistration(...)` 回归,覆盖“无匹配注册返回 false”与“开放泛型注册可满足封闭请求行为类型”两个分支 + - `GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs` 现在同时对照 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` + - `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs` 新增 `CreateMediatorServiceProvider(...)`,统一最小宿主构建方式 +- 本轮权威验证: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`51/51` passed + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:steady-state request 对照约为 baseline `5.969 ns / 32 B`、`Mediator` `6.242 ns / 32 B`、`MediatR` `53.818 ns / 232 B`、`GFramework.Cqrs` `85.504 ns / 32 B` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`Singleton` 下 `GFramework.Cqrs` 从旧值 `301.731 ns / 440 B` 收敛到 `84.066 ns / 32 B`;`Transient` 下从旧值 `287.863 ns / 464 B` 收敛到 `90.652 ns / 56 B` +- 本轮结论: + - `GFramework.Cqrs` 之前“垫底很多”的主要原因不是抽象层级本身,而是 request 热路径残留了两个可避免的分配热点:空 pipeline 解析与禁用日志下的字符串构造 + - 收口后,`GFramework.Cqrs` 仍慢于 `MediatR` 与 source-generated `Mediator`,但已经去掉了旧 benchmark 中最明显的异常分配和 300ns 级退化 + - 下一批若继续沿用 `$gframework-batch-boot 50` 压 request steady-state,最值得优先评估的是让默认 request 路径进一步吸收 generated invoker/provider 的收益,而不是继续扩大更多横向对照项 + ## 2026-05-07 ### 阶段:PR #339 stream pipeline seam review 收口(CQRS-REWRITE-RP-100)