refactor(cqrs): 收敛生成注册器激活反射路径

- 优化 generated registry 激活流程,使用缓存工厂委托优先替代 ConstructorInfo.Invoke\n- 补充私有无参构造 registry 的回归测试,保持生成器产物兼容性\n- 更新 CQRS ai-plan 恢复点与验证记录,指向新的 Phase 8 下一步
This commit is contained in:
gewuyou 2026-04-20 09:28:19 +08:00 committed by GeWuYou
parent a926748def
commit 7cf0a75568
4 changed files with 124 additions and 12 deletions

View File

@ -140,6 +140,31 @@ internal sealed class CqrsHandlerRegistrarTests
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
} }
/// <summary>
/// 验证 generated registry 使用私有无参构造器时,运行时仍可激活它并完成处理器注册。
/// </summary>
[Test]
public void RegisterHandlers_Should_Activate_Generated_Registry_With_Private_Parameterless_Constructor()
{
var generatedAssembly = new Mock<Assembly>();
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<INotificationHandler<GeneratedRegistryNotification>>();
Assert.That(
handlers.Select(static handler => handler.GetType()),
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
}
/// <summary> /// <summary>
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
/// </summary> /// </summary>
@ -608,3 +633,33 @@ internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandler
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}."); $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
} }
} }
/// <summary>
/// 模拟生成注册器使用私有无参构造器的场景,验证运行时仍可通过缓存工厂激活它。
/// </summary>
internal sealed class PrivateConstructorNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 初始化一个新的私有生成注册器实例。
/// </summary>
private PrivateConstructorNotificationHandlerRegistry()
{
}
/// <summary>
/// 将测试通知处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}

View File

@ -1,6 +1,7 @@
using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs;
using System.Reflection.Emit;
namespace GFramework.Cqrs.Internal; namespace GFramework.Cqrs.Internal;
@ -323,7 +324,51 @@ internal static class CqrsHandlerRegistrar
: new RegistryActivationMetadata( : new RegistryActivationMetadata(
true, true,
false, false,
() => (ICqrsHandlerRegistry)constructor.Invoke(null)); CreateRegistryFactory(registryType, constructor));
}
/// <summary>
/// 为生成注册器创建可复用的激活工厂,优先使用一次性编译的动态方法,
/// 避免后续每次命中缓存时仍走 <see cref="ConstructorInfo" /> 的反射激活路径。
/// </summary>
/// <param name="registryType">生成注册器类型。</param>
/// <param name="constructor">已解析的无参构造函数。</param>
/// <returns>可直接实例化注册器的工厂委托。</returns>
private static Func<ICqrsHandlerRegistry> 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<ICqrsHandlerRegistry>)dynamicMethod.CreateDelegate(typeof(Func<ICqrsHandlerRegistry>));
}
catch
{
// 某些受限运行环境若不允许动态方法,仍保留原有的反射激活语义,避免阻塞 generated registry 路径。
return () => (ICqrsHandlerRegistry)constructor.Invoke(null);
}
} }
/// <summary> /// <summary>

View File

@ -7,12 +7,13 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-045` - 恢复点编号:`CQRS-REWRITE-RP-046`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前焦点: - 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口 - 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
- 已完成 `PR #253` 的 latest head review thread 复核,确认远端剩余 open thread 属于未关闭的 stale review 噪音 - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,扩大 generator 覆盖、减少 dispatch/invoker 热路径反射,并继续收口 package / facade / 兼容层 - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
## 当前状态摘要 ## 当前状态摘要
@ -29,7 +30,11 @@ CQRS 迁移与收敛。
- `PR #253` 当前状态为 `CLOSED` - `PR #253` 当前状态为 `CLOSED`
- latest reviewed commit 仍显示 `1` 条 open thread但其内容针对的是已过时的 `Phase 7` 恢复建议 - latest reviewed commit 仍显示 `1` 条 open thread但其内容针对的是已过时的 `Phase 7` 恢复建议
- 当前 active tracking / trace 已统一到 `Phase 8`,因此该 thread 不再作为当前主线阻塞项 - 当前 active tracking / trace 已统一到 `Phase 8`,因此该 thread 不再作为当前主线阻塞项
- 若 PR review 噪音已收敛,再回到以下主线优先级: - `2026-04-20` 已完成一轮冷启动反射收敛:
- generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke`
- 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径
- `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖
- 当前主线优先级:
- generator 覆盖面继续扩大 - generator 覆盖面继续扩大
- dispatch/invoker 反射占比继续下降 - dispatch/invoker 反射占比继续下降
- package / facade / 兼容层继续收口 - package / facade / 兼容层继续收口
@ -50,9 +55,12 @@ CQRS 迁移与收敛。
- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档
- active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace - 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 / 命名空间表述 2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述
3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤 3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤

View File

@ -2,12 +2,16 @@
## 2026-04-20 ## 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` - 已在 `CqrsHandlerRegistrar` 中将 generated registry 的无参构造激活改为类型级缓存工厂
- latest reviewed commit 仍显示 `1` 条 open thread但评论指向的是 tracking 文件中已经修正的旧版 `Phase 7` 恢复建议 - 默认路径优先使用一次性动态方法直接创建 registry避免后续每次命中缓存仍走 `ConstructorInfo.Invoke`
- 复核当前 active tracking / trace 后确认:默认 boot 入口已经统一到 `Phase 8`,该 thread 属于未关闭的 stale review 噪音 - 若运行环境不允许动态方法,则保留原有反射激活回退,确保 generated registry 路径不因运行时限制失效
- 当前功能主线恢复为 `Phase 8` 的 generator / dispatch / package 收口工作 - 已补充“私有无参构造 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 ### Archive Context
@ -18,6 +22,6 @@
### 当前下一步 ### 当前下一步
1. 回到 `Phase 8` 主线,优先选一个明确的反射缩减点继续推进 1. 回到 `Phase 8` 主线,优先选一个明确的 dispatch / invoker 反射缩减点继续推进
2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述 2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述
3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号 3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号