perf(cqrs): 收敛生成器 fallback 元数据发射

- 优化 CqrsHandlerRegistryGenerator 的 fallback 合同探测与元数据发射策略,在可直接引用 handlers 时优先输出 Type 元数据
- 补充 SourceGenerators 回归测试,覆盖字符串合同兼容路径与直接 Type 元数据优先级
- 更新 CQRS 生成器说明与 ai-plan 恢复文档,记录 RP-051 的验证结果与后续方向
This commit is contained in:
gewuyou 2026-04-29 13:25:20 +08:00
parent 4557dde631
commit 5fd71f3620
8 changed files with 356 additions and 47 deletions

View File

@ -154,6 +154,26 @@ public sealed partial class CqrsHandlerRegistryGenerator
string HandlerInterfaceLogName,
ImmutableArray<RuntimeTypeReferenceSpec> ServiceTypeArguments);
/// <summary>
/// 描述本轮生成应如何发射程序集级 reflection fallback 元数据。
/// </summary>
/// <remarks>
/// 生成器会优先尝试使用 <c>typeof(...)</c> 形式的 <see cref="Type" /> 元数据,
/// 以减少运行时再做字符串类型名回查的成本;但当任一 fallback handler 仍无法被生成代码直接引用时,
/// 会整体回退到字符串元数据,避免 mixed 场景下遗漏剩余 handler。
/// </remarks>
private readonly record struct ReflectionFallbackEmissionSpec(
bool EmitDirectTypeReferences,
ImmutableArray<string> HandlerTypeDisplayNames,
ImmutableArray<string> HandlerTypeMetadataNames)
{
/// <summary>
/// 获取当前是否需要发射任何 fallback 元数据。
/// </summary>
public bool HasFallbackHandlers =>
!HandlerTypeDisplayNames.IsDefaultOrEmpty || !HandlerTypeMetadataNames.IsDefaultOrEmpty;
}
private readonly record struct ImplementationRegistrationSpec(
string ImplementationTypeDisplayName,
string ImplementationLogName,
@ -161,6 +181,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations,
ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations,
string? ReflectionTypeMetadataName,
string? ReflectionFallbackHandlerTypeDisplayName,
string? ReflectionFallbackHandlerTypeMetadataName);
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
@ -172,6 +193,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
ImmutableArray<ReflectedImplementationRegistrationSpec> reflectedImplementationRegistrations,
ImmutableArray<PreciseReflectedRegistrationSpec> preciseReflectedRegistrations,
string? reflectionTypeMetadataName,
string? reflectionFallbackHandlerTypeDisplayName,
string? reflectionFallbackHandlerTypeMetadataName)
{
ImplementationTypeDisplayName = implementationTypeDisplayName;
@ -180,6 +202,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
ReflectedImplementationRegistrations = reflectedImplementationRegistrations;
PreciseReflectedRegistrations = preciseReflectedRegistrations;
ReflectionTypeMetadataName = reflectionTypeMetadataName;
ReflectionFallbackHandlerTypeDisplayName = reflectionFallbackHandlerTypeDisplayName;
ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName;
}
@ -195,6 +218,8 @@ public sealed partial class CqrsHandlerRegistryGenerator
public string? ReflectionTypeMetadataName { get; }
public string? ReflectionFallbackHandlerTypeDisplayName { get; }
public string? ReflectionFallbackHandlerTypeMetadataName { get; }
public bool Equals(HandlerCandidateAnalysis other)
@ -204,6 +229,10 @@ public sealed partial class CqrsHandlerRegistryGenerator
!string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) ||
!string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName,
StringComparison.Ordinal) ||
!string.Equals(
ReflectionFallbackHandlerTypeDisplayName,
other.ReflectionFallbackHandlerTypeDisplayName,
StringComparison.Ordinal) ||
!string.Equals(
ReflectionFallbackHandlerTypeMetadataName,
other.ReflectionFallbackHandlerTypeMetadataName,
@ -254,6 +283,10 @@ public sealed partial class CqrsHandlerRegistryGenerator
(ReflectionTypeMetadataName is null
? 0
: StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName));
hashCode = (hashCode * 397) ^
(ReflectionFallbackHandlerTypeDisplayName is null
? 0
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeDisplayName));
hashCode = (hashCode * 397) ^
(ReflectionFallbackHandlerTypeMetadataName is null
? 0
@ -280,5 +313,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
private readonly record struct GenerationEnvironment(
bool GenerationEnabled,
bool SupportsReflectionFallbackAttribute);
bool SupportsNamedReflectionFallbackTypes,
bool SupportsDirectReflectionFallbackTypes);
}

View File

@ -14,25 +14,27 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// <param name="registrations">
/// 已整理并排序的 handler 注册描述。方法会据此生成 <c>CqrsHandlerRegistry.g.cs</c>,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。
/// </param>
/// <param name="fallbackHandlerTypeMetadataNames">
/// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合
/// 调用方必须先确保:若该集合非空,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
/// <param name="reflectionFallbackEmission">
/// 当前轮次选定的程序集级 reflection fallback 元数据发射策略
/// 调用方必须先确保:若该策略包含 fallback handlers,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
/// </param>
/// <returns>完整的注册器源代码文本。</returns>
/// <remarks>
/// 当 <paramref name="fallbackHandlerTypeMetadataNames" /> 为空时,输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
/// 当其非空且 runtime 合同可用时,输出还会附带程序集级 <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
/// 当 <paramref name="reflectionFallbackEmission" /> 不包含任何 fallback handlers 时,
/// 输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
/// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带程序集级
/// <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
/// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
/// </remarks>
private static string GenerateSource(
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
var builder = new StringBuilder();
AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames);
AppendGeneratedSourcePreamble(builder, generationEnvironment, reflectionFallbackEmission);
AppendGeneratedRegistryType(builder, registrations, sourceShape);
return builder.ToString();
}
@ -67,18 +69,18 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="generationEnvironment">当前轮次的生成环境。</param>
/// <param name="fallbackHandlerTypeMetadataNames">需要程序集级 reflection fallback 的 handler 元数据名称。</param>
/// <param name="reflectionFallbackEmission">需要写入程序集级 reflection fallback 特性的元数据策略。</param>
private static void AppendGeneratedSourcePreamble(
StringBuilder builder,
GenerationEnvironment generationEnvironment,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
builder.AppendLine();
if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0)
if (reflectionFallbackEmission.HasFallbackHandlers)
{
AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames);
AppendReflectionFallbackAttribute(builder, reflectionFallbackEmission);
builder.AppendLine();
}
@ -95,22 +97,36 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="fallbackHandlerTypeMetadataNames">需要写入特性的 handler 元数据名称。</param>
/// <param name="reflectionFallbackEmission">需要写入特性的 fallback 元数据策略。</param>
private static void AppendReflectionFallbackAttribute(
StringBuilder builder,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
builder.Append("[assembly: global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsReflectionFallbackAttribute(");
for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++)
var fallbackValues = reflectionFallbackEmission.EmitDirectTypeReferences
? reflectionFallbackEmission.HandlerTypeDisplayNames
: reflectionFallbackEmission.HandlerTypeMetadataNames;
for (var index = 0; index < fallbackValues.Length; index++)
{
if (index > 0)
builder.Append(", ");
builder.Append('"');
builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index]));
builder.Append('"');
if (reflectionFallbackEmission.EmitDirectTypeReferences)
{
builder.Append("typeof(");
builder.Append(fallbackValues[index]);
builder.Append(')');
}
else
{
builder.Append('"');
builder.Append(EscapeStringLiteral(fallbackValues[index]));
builder.Append('"');
}
}
builder.AppendLine(")]");

View File

@ -56,6 +56,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
private static GenerationEnvironment CreateGenerationEnvironment(Compilation compilation)
{
var reflectionFallbackAttributeType =
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName);
var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
@ -64,10 +66,22 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
CqrsHandlerRegistryAttributeMetadataName) is not null &&
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
var supportsReflectionFallbackAttribute =
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null;
var stringType = compilation.GetSpecialType(SpecialType.System_String);
var typeType = compilation.GetTypeByMetadataName("System.Type");
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
HasParamsArrayConstructor(
reflectionFallbackAttributeType,
stringType);
var supportsDirectReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
typeType is not null &&
HasParamsArrayConstructor(
reflectionFallbackAttributeType,
typeType);
return new GenerationEnvironment(generationEnabled, supportsReflectionFallbackAttribute);
return new GenerationEnvironment(
generationEnabled,
supportsNamedReflectionFallbackTypes,
supportsDirectReflectionFallbackTypes);
}
private static bool IsHandlerCandidate(SyntaxNode node)
@ -130,6 +144,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ImmutableArray.CreateBuilder<ReflectedImplementationRegistrationSpec>(handlerInterfaces.Length);
var preciseReflectedRegistrations =
ImmutableArray.CreateBuilder<PreciseReflectedRegistrationSpec>(handlerInterfaces.Length);
string? reflectionFallbackHandlerTypeDisplayName = null;
string? reflectionFallbackHandlerTypeMetadataName = null;
foreach (var handlerInterface in handlerInterfaces)
{
@ -151,6 +166,9 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
// If a future Roslyn type shape still slips through this net, keep the generator conservative:
// preserve the static registrations we do understand, and let the runtime recover the remaining
// interfaces via the existing assembly-level targeted reflection fallback contract.
if (canReferenceImplementation)
reflectionFallbackHandlerTypeDisplayName ??= implementationTypeDisplayName;
reflectionFallbackHandlerTypeMetadataName ??= GetReflectionTypeMetadataName(type);
}
@ -161,6 +179,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
reflectedImplementationRegistrations.ToImmutable(),
preciseReflectedRegistrations.ToImmutable(),
canReferenceImplementation ? null : GetReflectionTypeMetadataName(type),
reflectionFallbackHandlerTypeDisplayName,
reflectionFallbackHandlerTypeMetadataName);
}
@ -259,57 +278,59 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
if (registrations.Count == 0)
return;
var fallbackHandlerTypeMetadataNames = registrations
.Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName)
.Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName))
.Distinct(StringComparer.Ordinal)
.Cast<string>()
.ToArray();
var reflectionFallbackEmission = CreateReflectionFallbackEmissionSpec(generationEnvironment, registrations);
if (!CanEmitGeneratedRegistry(
generationEnvironment.SupportsReflectionFallbackAttribute,
fallbackHandlerTypeMetadataNames.Length))
generationEnvironment,
reflectionFallbackEmission))
{
ReportMissingReflectionFallbackContractDiagnostic(
context,
fallbackHandlerTypeMetadataNames);
registrations);
return;
}
context.AddSource(
HintName,
GenerateSource(generationEnvironment, registrations, fallbackHandlerTypeMetadataNames));
GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission));
}
/// <summary>
/// 判断当前轮次是否允许输出生成注册器。
/// </summary>
/// <param name="supportsReflectionFallbackAttribute">
/// runtime 合同中是否存在 <c>CqrsReflectionFallbackAttribute</c>,以承载生成器无法静态精确表达的 handler 回退元数据。
/// </param>
/// <param name="fallbackHandlerTypeCount">
/// 当前轮次需要依赖程序集级 reflection fallback 元数据恢复的 handler 数量。
/// </param>
/// <param name="generationEnvironment">当前轮次可用的 fallback 合同能力。</param>
/// <param name="reflectionFallbackEmission">当前轮次选定的 fallback 元数据发射策略。</param>
/// <returns>
/// 当没有 handler 依赖 fallback或 runtime 已提供承载该元数据的特性契约时返回 <see langword="true" />
/// 当没有 handler 依赖 fallback或 runtime 已提供本轮策略所需的元数据承载重载时返回 <see langword="true" />
/// 否则返回 <see langword="false" />,调用方必须放弃生成以避免输出会静默漏注册的半成品注册器。
/// </returns>
private static bool CanEmitGeneratedRegistry(
bool supportsReflectionFallbackAttribute,
int fallbackHandlerTypeCount)
GenerationEnvironment generationEnvironment,
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute;
if (!reflectionFallbackEmission.HasFallbackHandlers)
return true;
return reflectionFallbackEmission.EmitDirectTypeReferences
? generationEnvironment.SupportsDirectReflectionFallbackTypes
: generationEnvironment.SupportsNamedReflectionFallbackTypes;
}
/// <summary>
/// 报告当前轮次因缺少 fallback 元数据承载契约而无法安全生成注册器的诊断。
/// </summary>
/// <param name="context">源生产上下文。</param>
/// <param name="fallbackHandlerTypeMetadataNames">需要通过程序集级 reflection fallback 元数据恢复的 handler 元数据名称。</param>
/// <param name="registrations">当前轮次汇总后的 handler 注册描述。</param>
private static void ReportMissingReflectionFallbackContractDiagnostic(
SourceProductionContext context,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
var fallbackHandlerTypeMetadataNames = registrations
.Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName)
.Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName))
.Distinct(StringComparer.Ordinal)
.Cast<string>()
.ToArray();
var handlerList = string.Join(
", ",
fallbackHandlerTypeMetadataNames.OrderBy(static name => name, StringComparer.Ordinal));
@ -346,6 +367,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
candidate.ReflectedImplementationRegistrations,
candidate.PreciseReflectedRegistrations,
candidate.ReflectionTypeMetadataName,
candidate.ReflectionFallbackHandlerTypeDisplayName,
candidate.ReflectionFallbackHandlerTypeMetadataName));
}
@ -354,6 +376,78 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return registrations;
}
/// <summary>
/// 选择本轮生成应采用的 fallback 元数据发射策略。
/// </summary>
/// <remarks>
/// 只有当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 <c>params Type[]</c> 重载时,
/// 才会选择直接 <see cref="Type" /> 元数据;否则统一回退到字符串类型名,避免 mixed 场景丢失剩余 handler。
/// </remarks>
private static ReflectionFallbackEmissionSpec CreateReflectionFallbackEmissionSpec(
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
var fallbackHandlerTypeMetadataNames = registrations
.Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName)
.Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName))
.Distinct(StringComparer.Ordinal)
.Cast<string>()
.ToImmutableArray();
if (fallbackHandlerTypeMetadataNames.IsDefaultOrEmpty)
{
return new ReflectionFallbackEmissionSpec(
EmitDirectTypeReferences: false,
HandlerTypeDisplayNames: ImmutableArray<string>.Empty,
HandlerTypeMetadataNames: ImmutableArray<string>.Empty);
}
var fallbackHandlerTypeDisplayNames = registrations
.Select(static registration => registration.ReflectionFallbackHandlerTypeDisplayName)
.Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName))
.Distinct(StringComparer.Ordinal)
.Cast<string>()
.ToImmutableArray();
if (generationEnvironment.SupportsDirectReflectionFallbackTypes &&
fallbackHandlerTypeDisplayNames.Length == fallbackHandlerTypeMetadataNames.Length)
{
return new ReflectionFallbackEmissionSpec(
EmitDirectTypeReferences: true,
HandlerTypeDisplayNames: fallbackHandlerTypeDisplayNames,
HandlerTypeMetadataNames: ImmutableArray<string>.Empty);
}
return new ReflectionFallbackEmissionSpec(
EmitDirectTypeReferences: false,
HandlerTypeDisplayNames: ImmutableArray<string>.Empty,
HandlerTypeMetadataNames: fallbackHandlerTypeMetadataNames);
}
/// <summary>
/// 判断目标特性是否暴露了指定元素类型的 <c>params T[]</c> 构造函数。
/// </summary>
private static bool HasParamsArrayConstructor(INamedTypeSymbol attributeType, ITypeSymbol elementType)
{
foreach (var constructor in attributeType.InstanceConstructors)
{
if (constructor.Parameters.Length != 1)
continue;
var parameter = constructor.Parameters[0];
if (!parameter.IsParams)
continue;
if (parameter.Type is IArrayTypeSymbol { Rank: 1 } arrayType &&
SymbolEqualityComparer.Default.Equals(arrayType.ElementType, elementType))
{
return true;
}
}
return false;
}
private static bool IsConcreteHandlerType(INamedTypeSymbol type)
{
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&

View File

@ -33,6 +33,7 @@
- `Cqrs/CqrsHandlerRegistryGenerator.cs`
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据,进一步减少运行时按类型名回查程序集的成本。
## 最小接入路径

View File

@ -1363,6 +1363,89 @@ public class CqrsHandlerRegistryGeneratorTests
}
""";
private const string AssemblyLevelDirectFallbackMetadataSource = """
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
public CqrsReflectionFallbackAttribute(params Type[] fallbackHandlerTypes) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed class Container
{
private unsafe struct AlphaResponse
{
}
private unsafe struct BetaResponse
{
}
private unsafe sealed record AlphaRequest() : IRequest<delegate* unmanaged<AlphaResponse>>;
private unsafe sealed record BetaRequest() : IRequest<delegate* unmanaged<BetaResponse>>;
public unsafe sealed class BetaHandler : IRequestHandler<BetaRequest, delegate* unmanaged<BetaResponse>>
{
}
public unsafe sealed class AlphaHandler : IRequestHandler<AlphaRequest, delegate* unmanaged<AlphaResponse>>
{
}
}
}
""";
/// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary>
@ -1690,6 +1773,45 @@ public class CqrsHandlerRegistryGeneratorTests
});
}
/// <summary>
/// 验证当所有 fallback handlers 本身都可直接引用,且 runtime 同时支持字符串与 <see cref="Type" /> 元数据承载时,
/// 生成器会优先发射直接 <c>typeof(...)</c> 的 fallback 特性,减少运行时按名称回查程序集类型。
/// </summary>
[Test]
public void
Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available()
{
var execution = ExecuteGenerator(
AssemblyLevelDirectFallbackMetadataSource,
allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
Assert.That(
execution.GeneratedSources[0].content,
Does.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler), typeof(global::TestApp.Container.BetaHandler))]"));
Assert.That(
execution.GeneratedSources[0].content,
Does.Not.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+AlphaHandler\", \"TestApp.Container+BetaHandler\")]"));
});
}
/// <summary>
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
/// </summary>

View File

@ -7,10 +7,12 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-050`
- 恢复点编号:`CQRS-REWRITE-RP-051`
- 当前阶段:`Phase 8`
- 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
- mixed fallback 场景继续整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
@ -51,6 +53,11 @@ CQRS 迁移与收敛。
- `CqrsHandlerRegistrar` 现会在单次 reflection 注册流程开始时构建已注册 handler 映射索引
- 同一批注册中后续 duplicate handler mapping 不再重复线性扫描 `IServiceCollection`
- `GFramework.Cqrs.Tests` 已补充“程序集返回重复 handler 类型时仍只注册一份映射”的回归
- `2026-04-29` 已完成一轮 generator fallback 元数据收敛:
- `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否同时支持 `params string[]``params Type[]` 两类 `CqrsReflectionFallbackAttribute` 构造函数
- 当本轮 fallback handlers 全部可被生成代码直接引用时,生成器会优先发射 `typeof(...)` 形式的程序集级 fallback 元数据,减少运行时 `Assembly.GetType(...)` 回查
- 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续统一发射字符串元数据,避免 mixed 场景只恢复部分 handlers
- `GFramework.SourceGenerators.Tests` 已补充 runtime 同时暴露两类构造函数时优先选择直接 `Type` 元数据的回归
- 当前主线优先级:
- generator 覆盖面继续扩大
- dispatch/invoker 反射占比继续下降
@ -64,7 +71,6 @@ CQRS 迁移与收敛。
## 活跃文档
- 模块拆分计划:`ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md`
- 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md)
- 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md)
@ -84,9 +90,15 @@ CQRS 迁移与收敛。
- `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 去重路径
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级
## 下一步
1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口或 dispatch / invoker 反射收敛点继续推进
1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍依赖程序集级 fallback 字符串元数据的场景
2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述
3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤

View File

@ -2,6 +2,34 @@
## 2026-04-20
### 阶段direct fallback 元数据优先级收敛CQRS-REWRITE-RP-051
- 重新按 `gframework-batch-boot 50` 恢复 `Phase 8` 后,先复核当前 worktree 的恢复入口、`origin/main` 基线与分支规模:
- worktree 仍映射到 `cqrs-rewrite`
- 基线按批处理约定固定为 `origin/main`
- 本轮开始前分支累计 diff 为 `0 files / 0 lines`
- 结合当前代码热点与历史归档后,选择本轮批次目标为“继续收敛 generator fallback 元数据,进一步减少 runtime 按字符串类型名回查 handler 的场景”
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中新增 runtime fallback 合同探测:
- 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params string[]`
- 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params Type[]`
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs`
`CqrsHandlerRegistryGenerator.SourceEmission.cs` 中收敛 fallback 发射策略:
- 当本轮所有 fallback handlers 都可被生成代码直接引用,且 runtime 支持 `params Type[]` 时,生成器现优先发射 `typeof(...)` 形式的程序集级 fallback 元数据
- 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续整体回退到字符串元数据,避免 mixed 场景下部分 handler 走 `Type[]`、其余 handler 丢失恢复入口
- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充回归:
- 锁定 runtime 同时暴露字符串与 `Type` 两类 fallback 构造函数时,生成器优先选择直接 `Type` 元数据
- 保留现有字符串 fallback 合同测试,确保旧 contract 兼容路径不回退
- 同步更新:
- `GFramework.Cqrs.SourceGenerators/README.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 说明“可直接引用的 fallback handlers 会优先走 `typeof(...)` 元数据,减少运行时字符串回查”
- 定向验证已通过:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `17/17` passed
- 额外修正:
- active tracking 中原先引用的 `ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md` 在当前 worktree 已不存在;本轮已移除该失效路径,后续以 active tracking / trace 作为默认恢复入口
### 阶段pointer / function pointer 泛型合同拒绝CQRS-REWRITE-RP-050
- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`,状态仍为 `OPEN`
@ -72,6 +100,6 @@
### 当前下一步
1. 回到 `Phase 8` 主线,优先选一个明确的 dispatch / invoker 反射缩减点继续推进
1. 回到 `Phase 8` 主线,优先再找一个 generator 覆盖缺口,继续减少仍需程序集级字符串 fallback 元数据的 handler 场景
2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述
3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号

View File

@ -32,6 +32,7 @@ runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整
- 程序集级 `CqrsReflectionFallbackAttribute`
这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。
如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据,进一步减少 runtime 再做字符串类型名回查的成本。
## 最小接入路径
@ -108,6 +109,7 @@ RegisterCqrsHandlersFromAssemblies(
- 能直接引用的 handler生成直接注册语句
- 实现类型不能直接引用、但服务接口还能精确表达时,生成反射实现类型查找
- 服务接口本身也需要运行时解析时,生成精确 type lookup
- 当 fallback handlers 全部可直接引用且 runtime 暴露 `params Type[]` 合同时,优先发射直接 `Type` 元数据;否则统一回退到字符串元数据,避免 mixed 场景漏注册
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
如果当前编译环境缺少这个 fallback 合同,而某些 handler 又必须依赖它,生成器会报: