diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 3e92281d..349c80f7 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -140,6 +140,31 @@ internal sealed class CqrsHandlerRegistrarTests Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); } + /// + /// 验证 generated registry 使用私有无参构造器时,运行时仍可激活它并完成处理器注册。 + /// + [Test] + public void RegisterHandlers_Should_Activate_Generated_Registry_With_Private_Parameterless_Constructor() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.PrivateGeneratedRegistryAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PrivateConstructorNotificationHandlerRegistry))]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var handlers = container.GetAll>(); + + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + } + /// /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 /// @@ -608,3 +633,33 @@ internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandler $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); } } + +/// +/// 模拟生成注册器使用私有无参构造器的场景,验证运行时仍可通过缓存工厂激活它。 +/// +internal sealed class PrivateConstructorNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 初始化一个新的私有生成注册器实例。 + /// + private PrivateConstructorNotificationHandlerRegistry() + { + } + + /// + /// 将测试通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 031cb7e4..c9562b17 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -1,6 +1,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; +using System.Reflection.Emit; namespace GFramework.Cqrs.Internal; @@ -323,7 +324,51 @@ internal static class CqrsHandlerRegistrar : new RegistryActivationMetadata( true, false, - () => (ICqrsHandlerRegistry)constructor.Invoke(null)); + CreateRegistryFactory(registryType, constructor)); + } + + /// + /// 为生成注册器创建可复用的激活工厂,优先使用一次性编译的动态方法, + /// 避免后续每次命中缓存时仍走 的反射激活路径。 + /// + /// 生成注册器类型。 + /// 已解析的无参构造函数。 + /// 可直接实例化注册器的工厂委托。 + private static Func CreateRegistryFactory( + Type registryType, + ConstructorInfo constructor) + { + ArgumentNullException.ThrowIfNull(registryType); + ArgumentNullException.ThrowIfNull(constructor); + + try + { + // 生成器产物通常是稳定的无参 registry;这里把构造反射收敛为一次性 IL 工厂, + // 这样同一 registry 类型在多个容器间复用缓存时不会重复付出 ConstructorInfo.Invoke 成本。 + var dynamicMethod = new DynamicMethod( + $"Create_{registryType.Name}_CqrsHandlerRegistry", + typeof(ICqrsHandlerRegistry), + Type.EmptyTypes, + registryType.Module, + skipVisibility: true); + var il = dynamicMethod.GetILGenerator(); + il.Emit(OpCodes.Newobj, constructor); + + if (registryType.IsValueType) + { + il.Emit(OpCodes.Box, registryType); + } + + il.Emit(OpCodes.Castclass, typeof(ICqrsHandlerRegistry)); + il.Emit(OpCodes.Ret); + + return (Func)dynamicMethod.CreateDelegate(typeof(Func)); + } + catch + { + // 某些受限运行环境若不允许动态方法,仍保留原有的反射激活语义,避免阻塞 generated registry 路径。 + return () => (ICqrsHandlerRegistry)constructor.Invoke(null); + } } /// 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 c4c68a2f..7e4a4198 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,12 +7,13 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-045` +- 恢复点编号:`CQRS-REWRITE-RP-046` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 - - 已完成 `PR #253` 的 latest head review thread 复核,确认远端剩余 open thread 属于未关闭的 stale review 噪音 - - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,扩大 generator 覆盖、减少 dispatch/invoker 热路径反射,并继续收口 package / facade / 兼容层 + - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` + - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 + - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 ## 当前状态摘要 @@ -29,7 +30,11 @@ CQRS 迁移与收敛。 - `PR #253` 当前状态为 `CLOSED` - latest reviewed commit 仍显示 `1` 条 open thread,但其内容针对的是已过时的 `Phase 7` 恢复建议 - 当前 active tracking / trace 已统一到 `Phase 8`,因此该 thread 不再作为当前主线阻塞项 -- 若 PR review 噪音已收敛,再回到以下主线优先级: +- `2026-04-20` 已完成一轮冷启动反射收敛: + - generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke` + - 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径 + - `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖 +- 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 - package / facade / 兼容层继续收口 @@ -50,9 +55,12 @@ CQRS 迁移与收敛。 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` + - 结果:通过 + - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行 ## 下一步 -1. 回到 `Phase 8` 主线,优先选择一个收益明确的反射收敛点继续推进 +1. 继续 `Phase 8` 主线,优先选择下一个收益明确的 dispatch / invoker 反射收敛点继续推进 2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$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 65de190d..9353cdd9 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,12 +2,16 @@ ## 2026-04-20 -### 阶段:PR #253 latest head review thread 复核(CQRS-REWRITE-RP-045) +### 阶段:generated registry 激活反射收敛(CQRS-REWRITE-RP-046) -- 已重新执行 `$gframework-pr-review`,确认 `PR #253` 当前状态为 `CLOSED` -- latest reviewed commit 仍显示 `1` 条 open thread,但评论指向的是 tracking 文件中已经修正的旧版 `Phase 7` 恢复建议 -- 复核当前 active tracking / trace 后确认:默认 boot 入口已经统一到 `Phase 8`,该 thread 属于未关闭的 stale review 噪音 -- 当前功能主线恢复为 `Phase 8` 的 generator / dispatch / package 收口工作 +- 已在 `CqrsHandlerRegistrar` 中将 generated registry 的无参构造激活改为类型级缓存工厂 +- 默认路径优先使用一次性动态方法直接创建 registry,避免后续每次命中缓存仍走 `ConstructorInfo.Invoke` +- 若运行环境不允许动态方法,则保留原有反射激活回退,确保 generated registry 路径不因运行时限制失效 +- 已补充“私有无参构造 generated registry 仍可激活”的回归测试,覆盖现有生成器产物兼容性 +- 定向验证已通过: + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false` + - `63/63` passed + - 当前沙箱限制 MSBuild named pipe,因此验证在提权环境下执行 ### Archive Context @@ -18,6 +22,6 @@ ### 当前下一步 -1. 回到 `Phase 8` 主线,优先选一个明确的反射缩减点继续推进 +1. 回到 `Phase 8` 主线,优先选一个明确的 dispatch / invoker 反射缩减点继续推进 2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述 3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号