fix(cqrs): 修复指针合同生成回归

- 修复 CqrsHandlerRegistryGenerator 对 pointer 与 function pointer 的精确注册建模
- 补充生成器测试对输入源 CS0306 与 fallback 诊断的断言
- 更新 cqrs-rewrite 跟踪文档记录 PR #261 review follow-up
This commit is contained in:
GeWuYou 2026-04-20 17:55:41 +08:00
parent db65249315
commit c1f9fa8b9a
4 changed files with 75 additions and 77 deletions

View File

@ -398,6 +398,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ITypeSymbol type, ITypeSymbol type,
out RuntimeTypeReferenceSpec? runtimeTypeReference) out RuntimeTypeReferenceSpec? runtimeTypeReference)
{ {
// CLR forbids pointer and function-pointer types from being used as generic arguments.
// CQRS handler contracts are generic interfaces, so emitting runtime reconstruction code for these
// shapes would only defer the failure to MakeGenericType(...) at runtime.
if (type is IPointerTypeSymbol or IFunctionPointerTypeSymbol)
{
runtimeTypeReference = null;
return false;
}
if (CanReferenceFromGeneratedRegistry(compilation, type)) if (CanReferenceFromGeneratedRegistry(compilation, type))
{ {
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference(
@ -412,13 +421,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return true; return true;
} }
if (type is IPointerTypeSymbol pointerType &&
TryCreateRuntimeTypeReference(compilation, pointerType.PointedAtType, out var pointedAtTypeReference))
{
runtimeTypeReference = RuntimeTypeReferenceSpec.FromPointer(pointedAtTypeReference!);
return true;
}
if (type is INamedTypeSymbol genericNamedType && if (type is INamedTypeSymbol genericNamedType &&
genericNamedType.IsGenericType && genericNamedType.IsGenericType &&
!genericNamedType.IsUnboundGenericType && !genericNamedType.IsUnboundGenericType &&
@ -525,19 +527,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
} }
return true; return true;
case IPointerTypeSymbol pointerType: case IPointerTypeSymbol:
return CanReferenceFromGeneratedRegistry(compilation, pointerType.PointedAtType); case IFunctionPointerTypeSymbol:
case IFunctionPointerTypeSymbol functionPointerType: return false;
if (!CanReferenceFromGeneratedRegistry(compilation, functionPointerType.Signature.ReturnType))
return false;
foreach (var parameter in functionPointerType.Signature.Parameters)
{
if (!CanReferenceFromGeneratedRegistry(compilation, parameter.Type))
return false;
}
return true;
case ITypeParameterSymbol: case ITypeParameterSymbol:
return false; return false;
default: default:

View File

@ -164,45 +164,6 @@ public class CqrsHandlerRegistryGeneratorTests
"""; """;
private const string HiddenPointerResponseExpected = """
// <auto-generated />
#nullable enable
[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]
namespace GFramework.Generated.Cqrs;
internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry
{
public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger)
{
if (services is null)
throw new global::System.ArgumentNullException(nameof(services));
if (logger is null)
throw new global::System.ArgumentNullException(nameof(logger));
var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly;
var implementationType0 = typeof(global::TestApp.Container.HiddenHandler);
if (implementationType0 is not null)
{
var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false);
var serviceType0_0Argument1PointedAt = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false);
if (serviceType0_0Argument0 is not null && serviceType0_0Argument1PointedAt is not null)
{
var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1PointedAt.MakePointerType());
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
services,
serviceType0_0,
implementationType0);
logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<TestApp.Container.HiddenRequest, TestApp.Container.HiddenResponse*>.");
}
}
}
}
""";
private const string MixedDirectAndPreciseRegistrationsExpected = """ private const string MixedDirectAndPreciseRegistrationsExpected = """
// <auto-generated /> // <auto-generated />
#nullable enable #nullable enable
@ -785,11 +746,11 @@ public class CqrsHandlerRegistryGeneratorTests
} }
/// <summary> /// <summary>
/// 验证精确重建路径会递归覆盖隐藏指针元素类型 /// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时
/// 使“隐藏 pointed-at 类型 + unsafe 指针响应”的 handler 也能直接生成 closed service type /// 生成器会保守回退而不是继续发射不可构造的精确注册代码
/// </summary> /// </summary>
[Test] [Test]
public async Task Generates_Precise_Service_Type_For_Hidden_Pointer_Response() public void Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response()
{ {
const string source = """ const string source = """
using System; using System;
@ -859,20 +820,31 @@ public class CqrsHandlerRegistryGeneratorTests
var execution = ExecuteGenerator( var execution = ExecuteGenerator(
source, source,
allowUnsafe: true); allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
var generatorErrors = execution.GeneratorDiagnostics var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic =>
string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty); Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(missingContractDiagnostic, Is.Not.Null);
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); Assert.That(
Assert.That(execution.GeneratedSources[0].content, Is.EqualTo(HiddenPointerResponseExpected)); missingContractDiagnostic!.GetMessage(),
Does.Contain("TestApp.Container+HiddenHandler"));
Assert.That(
missingContractDiagnostic.GetMessage(),
Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
}); });
} }
@ -1375,6 +1347,9 @@ public class CqrsHandlerRegistryGeneratorTests
var execution = ExecuteGenerator( var execution = ExecuteGenerator(
source, source,
allowUnsafe: true); allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
@ -1382,10 +1357,12 @@ public class CqrsHandlerRegistryGeneratorTests
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
var missingContractDiagnostic = var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic => diagnostic.Id == "GF_Cqrs_001"); generatorErrors.SingleOrDefault(static diagnostic =>
string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Is.Empty); Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(missingContractDiagnostic, Is.Not.Null); Assert.That(missingContractDiagnostic, Is.Not.Null);
@ -1490,6 +1467,9 @@ public class CqrsHandlerRegistryGeneratorTests
var execution = ExecuteGenerator( var execution = ExecuteGenerator(
source, source,
allowUnsafe: true); allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
@ -1499,6 +1479,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
@ -1611,6 +1592,11 @@ public class CqrsHandlerRegistryGeneratorTests
(filename: sourceResult.HintName, content: sourceResult.SourceText.ToString())) (filename: sourceResult.HintName, content: sourceResult.SourceText.ToString()))
.ToArray(); .ToArray();
var compilationDiagnostics = updatedCompilation.GetDiagnostics().ToArray(); var compilationDiagnostics = updatedCompilation.GetDiagnostics().ToArray();
var inputCompilationDiagnostics = compilationDiagnostics
.Where(diagnostic =>
diagnostic.Location.SourceTree is null ||
!generatedSyntaxTrees.Contains(diagnostic.Location.SourceTree))
.ToArray();
var generatedCompilationDiagnostics = compilationDiagnostics var generatedCompilationDiagnostics = compilationDiagnostics
.Where(diagnostic => .Where(diagnostic =>
diagnostic.Location.SourceTree is not null && diagnostic.Location.SourceTree is not null &&
@ -1620,6 +1606,7 @@ public class CqrsHandlerRegistryGeneratorTests
generatedSources, generatedSources,
generatorDiagnostics.ToArray(), generatorDiagnostics.ToArray(),
compilationDiagnostics, compilationDiagnostics,
inputCompilationDiagnostics,
generatedCompilationDiagnostics); generatedCompilationDiagnostics);
} }
@ -1629,10 +1616,12 @@ public class CqrsHandlerRegistryGeneratorTests
/// <param name="GeneratedSources">本轮生成产生的源文件集合。</param> /// <param name="GeneratedSources">本轮生成产生的源文件集合。</param>
/// <param name="GeneratorDiagnostics">生成器自身报告的诊断集合。</param> /// <param name="GeneratorDiagnostics">生成器自身报告的诊断集合。</param>
/// <param name="CompilationDiagnostics">将生成结果并回编译后的完整编译诊断集合。</param> /// <param name="CompilationDiagnostics">将生成结果并回编译后的完整编译诊断集合。</param>
/// <param name="InputCompilationDiagnostics">仅来自输入源文件的编译诊断集合。</param>
/// <param name="GeneratedCompilationDiagnostics">仅来自生成源文件的编译诊断集合。</param> /// <param name="GeneratedCompilationDiagnostics">仅来自生成源文件的编译诊断集合。</param>
private sealed record GeneratorExecutionResult( private sealed record GeneratorExecutionResult(
(string filename, string content)[] GeneratedSources, (string filename, string content)[] GeneratedSources,
Diagnostic[] GeneratorDiagnostics, Diagnostic[] GeneratorDiagnostics,
Diagnostic[] CompilationDiagnostics, Diagnostic[] CompilationDiagnostics,
Diagnostic[] InputCompilationDiagnostics,
Diagnostic[] GeneratedCompilationDiagnostics); Diagnostic[] GeneratedCompilationDiagnostics);
} }

View File

@ -7,14 +7,14 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-049` - 恢复点编号:`CQRS-REWRITE-RP-050`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前焦点: - 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口 - 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 已补充 pointer 响应类型的 precise runtime type 生成,避免这类 handler 再退回程序集级 reflection fallback - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
- 已收紧 function pointer 签名的可直接生成判定,仅在其返回值与参数类型都可安全引用时才走静态注册路径 - 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射 - 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找 - 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点 - 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
@ -31,17 +31,17 @@ CQRS 迁移与收敛。
- `Phase 8` 仍是当前主线,不再回退到 `Phase 7` - `Phase 8` 仍是当前主线,不再回退到 `Phase 7`
- `2026-04-20` 已重新执行 `$gframework-pr-review` - `2026-04-20` 已重新执行 `$gframework-pr-review`
- `PR #253` 当前状态为 `CLOSED` - 当前分支对应 `PR #261`,状态为 `OPEN`
- latest reviewed commit 仍显示 `1` 条 open thread但其内容针对的是已过时的 `Phase 7` 恢复建议 - latest reviewed commit 上有 `2` 条 open CodeRabbit thread均指向 pointer / function pointer 泛型合同处理
- 当前 active tracking / trace 已统一到 `Phase 8`,因此该 thread 不再作为当前主线阻塞项 - 本地已接受并修复这两条建议:生成器拒绝为 pointer / function pointer 生成 precise registration测试改为显式断言输入源 `CS0306`
- `2026-04-20` 已完成一轮冷启动反射收敛: - `2026-04-20` 已完成一轮冷启动反射收敛:
- generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke` - generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke`
- 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径 - 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径
- `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖 - `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖
- `2026-04-20` 已完成一轮 generator 覆盖面扩展: - `2026-04-20` 已完成一轮 generator 覆盖面扩展:
- `CqrsHandlerRegistryGenerator`可为 pointer 类型递归重建 runtime type并通过 `MakePointerType()` 生成精确 service type - `CqrsHandlerRegistryGenerator`会在 runtime type 建模入口直接拒绝 `IPointerTypeSymbol``IFunctionPointerTypeSymbol`
- function pointer 签名不再默认视为“可直接引用”;只有当返回值与每个参数类型都可从 generated registry 安全引用时,才允许直接生成 - `CanReferenceFromGeneratedRegistry` 不再递归判断 pointer / function pointer 的内部元素,而是统一返回 `false`
- 含隐藏类型的 function pointer handler 仍会保留原有 fallback / 诊断路径,避免此次覆盖扩展误伤已有回退边界 - 相关 source-generator 回归已改为区分输入源诊断与生成源诊断,避免把非法泛型合同误判为成功生成
- `2026-04-20` 已完成一轮 registrar reflection 路径收敛: - `2026-04-20` 已完成一轮 registrar reflection 路径收敛:
- `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表 - `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表
- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选 - 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选
@ -76,7 +76,10 @@ CQRS 迁移与收敛。
- 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe验证需在提权环境下运行 - 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe验证需在提权环境下运行
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过 - 结果:通过
- 备注:`14/14` 测试通过;本轮覆盖 pointer precise registration 与 function pointer fallback 边界 - 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过 - 结果:通过
- 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径 - 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径

View File

@ -2,6 +2,20 @@
## 2026-04-20 ## 2026-04-20
### 阶段pointer / function pointer 泛型合同拒绝CQRS-REWRITE-RP-050
- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`latest reviewed commit 上有 `2` 条仍未关闭的 CodeRabbit thread
- 本地核对后确认这两条评论有效:`CqrsHandlerRegistryGenerator` 之前会为 `IPointerTypeSymbol` 递归构造 `MakePointerType()`,而测试只校验生成源诊断,未显式暴露输入源 `CS0306`
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中收紧 `TryCreateRuntimeTypeReference``CanReferenceFromGeneratedRegistry`
- pointer / function pointer 现统一视为不可精确生成的 CQRS 泛型合同,生成器会保守回退到既有 fallback / diagnostic 路径,而不再发射运行时 `MakeGenericType(...)` 风险代码
- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补充输入源诊断分离,并将相关测试改为显式断言 `CS0306` 与 fallback / diagnostic 结果
- 定向验证已通过:
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
- `3/3` passed
- 扩展验证已通过:
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `14/14` passed
### 阶段registrar duplicate mapping 索引收敛CQRS-REWRITE-RP-049 ### 阶段registrar duplicate mapping 索引收敛CQRS-REWRITE-RP-049
- 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引 - 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引