From 5fd71f36203b541adb2f4ce698618e7bff04b3cf Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:25:20 +0800 Subject: [PATCH 1/3] =?UTF-8?q?perf(cqrs):=20=E6=94=B6=E6=95=9B=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=20fallback=20=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8F=91=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化 CqrsHandlerRegistryGenerator 的 fallback 合同探测与元数据发射策略,在可直接引用 handlers 时优先输出 Type 元数据 - 补充 SourceGenerators 回归测试,覆盖字符串合同兼容路径与直接 Type 元数据优先级 - 更新 CQRS 生成器说明与 ai-plan 恢复文档,记录 RP-051 的验证结果与后续方向 --- .../CqrsHandlerRegistryGenerator.Models.cs | 36 ++++- ...HandlerRegistryGenerator.SourceEmission.cs | 50 +++--- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 144 +++++++++++++++--- GFramework.Cqrs.SourceGenerators/README.md | 1 + .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 122 +++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 18 ++- .../traces/cqrs-rewrite-migration-trace.md | 30 +++- .../cqrs-handler-registry-generator.md | 2 + 8 files changed, 356 insertions(+), 47 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index 541ffcf6..24952c47 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -154,6 +154,26 @@ public sealed partial class CqrsHandlerRegistryGenerator string HandlerInterfaceLogName, ImmutableArray ServiceTypeArguments); + /// + /// 描述本轮生成应如何发射程序集级 reflection fallback 元数据。 + /// + /// + /// 生成器会优先尝试使用 typeof(...) 形式的 元数据, + /// 以减少运行时再做字符串类型名回查的成本;但当任一 fallback handler 仍无法被生成代码直接引用时, + /// 会整体回退到字符串元数据,避免 mixed 场景下遗漏剩余 handler。 + /// + private readonly record struct ReflectionFallbackEmissionSpec( + bool EmitDirectTypeReferences, + ImmutableArray HandlerTypeDisplayNames, + ImmutableArray HandlerTypeMetadataNames) + { + /// + /// 获取当前是否需要发射任何 fallback 元数据。 + /// + 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 ReflectedImplementationRegistrations, ImmutableArray PreciseReflectedRegistrations, string? ReflectionTypeMetadataName, + string? ReflectionFallbackHandlerTypeDisplayName, string? ReflectionFallbackHandlerTypeMetadataName); private readonly struct HandlerCandidateAnalysis : IEquatable @@ -172,6 +193,7 @@ public sealed partial class CqrsHandlerRegistryGenerator ImmutableArray reflectedImplementationRegistrations, ImmutableArray 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); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 5ebf97e7..a3801991 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -14,25 +14,27 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 已整理并排序的 handler 注册描述。方法会据此生成 CqrsHandlerRegistry.g.cs,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。 /// - /// - /// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。 - /// 调用方必须先确保:若该集合非空,则 已声明支持对应的 fallback attribute 契约; + /// + /// 当前轮次选定的程序集级 reflection fallback 元数据发射策略。 + /// 调用方必须先确保:若该策略包含 fallback handlers,则 已声明支持对应的 fallback attribute 契约; /// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。 /// /// 完整的注册器源代码文本。 /// - /// 当 为空时,输出只包含程序集级 CqrsHandlerRegistryAttribute 和注册器实现。 - /// 当其非空且 runtime 合同可用时,输出还会附带程序集级 CqrsReflectionFallbackAttribute,让运行时补齐生成阶段无法精确表达的剩余 handler。 + /// 当 不包含任何 fallback handlers 时, + /// 输出只包含程序集级 CqrsHandlerRegistryAttribute 和注册器实现。 + /// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带程序集级 + /// CqrsReflectionFallbackAttribute,让运行时补齐生成阶段无法精确表达的剩余 handler。 /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 /// private static string GenerateSource( GenerationEnvironment generationEnvironment, IReadOnlyList registrations, - IReadOnlyList 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 /// /// 生成源码构造器。 /// 当前轮次的生成环境。 - /// 需要程序集级 reflection fallback 的 handler 元数据名称。 + /// 需要写入程序集级 reflection fallback 特性的元数据策略。 private static void AppendGeneratedSourcePreamble( StringBuilder builder, GenerationEnvironment generationEnvironment, - IReadOnlyList fallbackHandlerTypeMetadataNames) + ReflectionFallbackEmissionSpec reflectionFallbackEmission) { builder.AppendLine("// "); 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。 /// /// 生成源码构造器。 - /// 需要写入特性的 handler 元数据名称。 + /// 需要写入特性的 fallback 元数据策略。 private static void AppendReflectionFallbackAttribute( StringBuilder builder, - IReadOnlyList 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(")]"); diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index ed0963e5..6bad934a 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -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(handlerInterfaces.Length); var preciseReflectedRegistrations = ImmutableArray.CreateBuilder(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() - .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)); } /// /// 判断当前轮次是否允许输出生成注册器。 /// - /// - /// runtime 合同中是否存在 CqrsReflectionFallbackAttribute,以承载生成器无法静态精确表达的 handler 回退元数据。 - /// - /// - /// 当前轮次需要依赖程序集级 reflection fallback 元数据恢复的 handler 数量。 - /// + /// 当前轮次可用的 fallback 合同能力。 + /// 当前轮次选定的 fallback 元数据发射策略。 /// - /// 当没有 handler 依赖 fallback,或 runtime 已提供承载该元数据的特性契约时返回 ; + /// 当没有 handler 依赖 fallback,或 runtime 已提供本轮策略所需的元数据承载重载时返回 ; /// 否则返回 ,调用方必须放弃生成以避免输出会静默漏注册的半成品注册器。 /// 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; } /// /// 报告当前轮次因缺少 fallback 元数据承载契约而无法安全生成注册器的诊断。 /// /// 源生产上下文。 - /// 需要通过程序集级 reflection fallback 元数据恢复的 handler 元数据名称。 + /// 当前轮次汇总后的 handler 注册描述。 private static void ReportMissingReflectionFallbackContractDiagnostic( SourceProductionContext context, - IReadOnlyList fallbackHandlerTypeMetadataNames) + IReadOnlyList registrations) { + var fallbackHandlerTypeMetadataNames = registrations + .Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName) + .Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName)) + .Distinct(StringComparer.Ordinal) + .Cast() + .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; } + /// + /// 选择本轮生成应采用的 fallback 元数据发射策略。 + /// + /// + /// 只有当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 params Type[] 重载时, + /// 才会选择直接 元数据;否则统一回退到字符串类型名,避免 mixed 场景丢失剩余 handler。 + /// + private static ReflectionFallbackEmissionSpec CreateReflectionFallbackEmissionSpec( + GenerationEnvironment generationEnvironment, + IReadOnlyList registrations) + { + var fallbackHandlerTypeMetadataNames = registrations + .Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName) + .Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName)) + .Distinct(StringComparer.Ordinal) + .Cast() + .ToImmutableArray(); + + if (fallbackHandlerTypeMetadataNames.IsDefaultOrEmpty) + { + return new ReflectionFallbackEmissionSpec( + EmitDirectTypeReferences: false, + HandlerTypeDisplayNames: ImmutableArray.Empty, + HandlerTypeMetadataNames: ImmutableArray.Empty); + } + + var fallbackHandlerTypeDisplayNames = registrations + .Select(static registration => registration.ReflectionFallbackHandlerTypeDisplayName) + .Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName)) + .Distinct(StringComparer.Ordinal) + .Cast() + .ToImmutableArray(); + + if (generationEnvironment.SupportsDirectReflectionFallbackTypes && + fallbackHandlerTypeDisplayNames.Length == fallbackHandlerTypeMetadataNames.Length) + { + return new ReflectionFallbackEmissionSpec( + EmitDirectTypeReferences: true, + HandlerTypeDisplayNames: fallbackHandlerTypeDisplayNames, + HandlerTypeMetadataNames: ImmutableArray.Empty); + } + + return new ReflectionFallbackEmissionSpec( + EmitDirectTypeReferences: false, + HandlerTypeDisplayNames: ImmutableArray.Empty, + HandlerTypeMetadataNames: fallbackHandlerTypeMetadataNames); + } + + /// + /// 判断目标特性是否暴露了指定元素类型的 params T[] 构造函数。 + /// + 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 && diff --git a/GFramework.Cqrs.SourceGenerators/README.md b/GFramework.Cqrs.SourceGenerators/README.md index d735a2a8..420952b2 100644 --- a/GFramework.Cqrs.SourceGenerators/README.md +++ b/GFramework.Cqrs.SourceGenerators/README.md @@ -33,6 +33,7 @@ - `Cqrs/CqrsHandlerRegistryGenerator.cs` 它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。 +当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据,进一步减少运行时按类型名回查程序集的成本。 ## 最小接入路径 diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 458eaa18..d6204e64 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -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 { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + 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>; + + private unsafe sealed record BetaRequest() : IRequest>; + + public unsafe sealed class BetaHandler : IRequestHandler> + { + } + + public unsafe sealed class AlphaHandler : IRequestHandler> + { + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -1690,6 +1773,45 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当所有 fallback handlers 本身都可直接引用,且 runtime 同时支持字符串与 元数据承载时, + /// 生成器会优先发射直接 typeof(...) 的 fallback 特性,减少运行时按名称回查程序集类型。 + /// + [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\")]")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// 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 86a99c7a..5ac56983 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,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` 作为独立验证步骤 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 307dcf55..28f3a4e0 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,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` 复核远端信号 diff --git a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md index 63b90ded..4366fedf 100644 --- a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md +++ b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md @@ -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 又必须依赖它,生成器会报: From 76fcdb82335505be732e2dafcacad516ac1c6070 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:37:05 +0800 Subject: [PATCH 2/3] =?UTF-8?q?perf(cqrs):=20=E6=8B=86=E5=88=86=E6=B7=B7?= =?UTF-8?q?=E5=90=88=20fallback=20=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化 CqrsReflectionFallbackAttribute 与生成器发射策略,在 mixed 场景下拆分 Type 与字符串 fallback 元数据 - 补充 CQRS runtime 与 SourceGenerators 回归测试,锁定多实例 fallback 特性和定向类型回查行为 - 更新 CQRS 生成器文档与 ai-plan 恢复记录,沉淀 RP-052 的验证结果与下一步 --- .../CqrsHandlerRegistryGenerator.Models.cs | 24 +- ...HandlerRegistryGenerator.SourceEmission.cs | 34 ++- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 222 +++++++++++++++--- GFramework.Cqrs.SourceGenerators/README.md | 2 +- .../Cqrs/CqrsHandlerRegistrarTests.cs | 58 +++++ ...ReflectionFallbackNotificationContainer.cs | 20 ++ .../CqrsReflectionFallbackAttribute.cs | 4 +- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 122 ++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 26 +- .../traces/cqrs-rewrite-migration-trace.md | 33 +++ .../cqrs-handler-registry-generator.md | 6 +- 11 files changed, 492 insertions(+), 59 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index 24952c47..e86ec1a0 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -155,23 +155,26 @@ public sealed partial class CqrsHandlerRegistryGenerator ImmutableArray ServiceTypeArguments); /// - /// 描述本轮生成应如何发射程序集级 reflection fallback 元数据。 + /// 描述单个程序集级 reflection fallback 特性实例的发射内容。 /// /// - /// 生成器会优先尝试使用 typeof(...) 形式的 元数据, - /// 以减少运行时再做字符串类型名回查的成本;但当任一 fallback handler 仍无法被生成代码直接引用时, - /// 会整体回退到字符串元数据,避免 mixed 场景下遗漏剩余 handler。 + /// 某些运行时合同允许生成器把可直接引用的 fallback handlers 与必须按名称恢复的 handlers + /// 拆成多个特性实例,以进一步减少运行时字符串查找成本。 /// - private readonly record struct ReflectionFallbackEmissionSpec( + private readonly record struct ReflectionFallbackAttributeEmissionSpec( bool EmitDirectTypeReferences, - ImmutableArray HandlerTypeDisplayNames, - ImmutableArray HandlerTypeMetadataNames) + ImmutableArray Values); + + /// + /// 描述本轮生成应如何发射程序集级 reflection fallback 元数据。 + /// + private readonly record struct ReflectionFallbackEmissionSpec( + ImmutableArray Attributes) { /// /// 获取当前是否需要发射任何 fallback 元数据。 /// - public bool HasFallbackHandlers => - !HandlerTypeDisplayNames.IsDefaultOrEmpty || !HandlerTypeMetadataNames.IsDefaultOrEmpty; + public bool HasFallbackHandlers => !Attributes.IsDefaultOrEmpty; } private readonly record struct ImplementationRegistrationSpec( @@ -314,5 +317,6 @@ public sealed partial class CqrsHandlerRegistryGenerator private readonly record struct GenerationEnvironment( bool GenerationEnabled, bool SupportsNamedReflectionFallbackTypes, - bool SupportsDirectReflectionFallbackTypes); + bool SupportsDirectReflectionFallbackTypes, + bool SupportsMultipleReflectionFallbackAttributes); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index a3801991..3391e6ad 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -23,7 +23,7 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 当 不包含任何 fallback handlers 时, /// 输出只包含程序集级 CqrsHandlerRegistryAttribute 和注册器实现。 - /// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带程序集级 + /// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带一个或多个程序集级 /// CqrsReflectionFallbackAttribute,让运行时补齐生成阶段无法精确表达的剩余 handler。 /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 /// @@ -80,7 +80,7 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(); if (reflectionFallbackEmission.HasFallbackHandlers) { - AppendReflectionFallbackAttribute(builder, reflectionFallbackEmission); + AppendReflectionFallbackAttributes(builder, reflectionFallbackEmission); builder.AppendLine(); } @@ -98,38 +98,48 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 生成源码构造器。 /// 需要写入特性的 fallback 元数据策略。 - private static void AppendReflectionFallbackAttribute( + private static void AppendReflectionFallbackAttributes( StringBuilder builder, ReflectionFallbackEmissionSpec reflectionFallbackEmission) + { + foreach (var attributeEmission in reflectionFallbackEmission.Attributes) + { + AppendReflectionFallbackAttribute(builder, attributeEmission); + builder.AppendLine(); + } + } + + /// + /// 发射单个程序集级 reflection fallback 元数据特性实例。 + /// + private static void AppendReflectionFallbackAttribute( + StringBuilder builder, + ReflectionFallbackAttributeEmissionSpec attributeEmission) { builder.Append("[assembly: global::"); builder.Append(CqrsRuntimeNamespace); builder.Append(".CqrsReflectionFallbackAttribute("); - var fallbackValues = reflectionFallbackEmission.EmitDirectTypeReferences - ? reflectionFallbackEmission.HandlerTypeDisplayNames - : reflectionFallbackEmission.HandlerTypeMetadataNames; - - for (var index = 0; index < fallbackValues.Length; index++) + for (var index = 0; index < attributeEmission.Values.Length; index++) { if (index > 0) builder.Append(", "); - if (reflectionFallbackEmission.EmitDirectTypeReferences) + if (attributeEmission.EmitDirectTypeReferences) { builder.Append("typeof("); - builder.Append(fallbackValues[index]); + builder.Append(attributeEmission.Values[index]); builder.Append(')'); } else { builder.Append('"'); - builder.Append(EscapeStringLiteral(fallbackValues[index])); + builder.Append(EscapeStringLiteral(attributeEmission.Values[index])); builder.Append('"'); } } - builder.AppendLine(")]"); + builder.Append(")]"); } /// diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 6bad934a..688fbc9a 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -77,11 +77,15 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator HasParamsArrayConstructor( reflectionFallbackAttributeType, typeType); + var supportsMultipleReflectionFallbackAttributes = reflectionFallbackAttributeType is not null && + SupportsMultipleAttributeInstances( + reflectionFallbackAttributeType); return new GenerationEnvironment( generationEnabled, supportsNamedReflectionFallbackTypes, - supportsDirectReflectionFallbackTypes); + supportsDirectReflectionFallbackTypes, + supportsMultipleReflectionFallbackAttributes); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -311,9 +315,21 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (!reflectionFallbackEmission.HasFallbackHandlers) return true; - return reflectionFallbackEmission.EmitDirectTypeReferences - ? generationEnvironment.SupportsDirectReflectionFallbackTypes - : generationEnvironment.SupportsNamedReflectionFallbackTypes; + foreach (var attributeEmission in reflectionFallbackEmission.Attributes) + { + if (attributeEmission.EmitDirectTypeReferences) + { + if (!generationEnvironment.SupportsDirectReflectionFallbackTypes) + return false; + + continue; + } + + if (!generationEnvironment.SupportsNamedReflectionFallbackTypes) + return false; + } + + return true; } /// @@ -380,48 +396,164 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator /// 选择本轮生成应采用的 fallback 元数据发射策略。 /// /// - /// 只有当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 params Type[] 重载时, - /// 才会选择直接 元数据;否则统一回退到字符串类型名,避免 mixed 场景丢失剩余 handler。 + /// 当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 params Type[] 重载时, + /// 优先输出单个直接 元数据特性;当 runtime 同时支持多个特性实例时, + /// mixed 场景会拆分成“直接 + 字符串类型名”两类特性;其余场景统一回退到字符串元数据。 /// private static ReflectionFallbackEmissionSpec CreateReflectionFallbackEmissionSpec( GenerationEnvironment generationEnvironment, IReadOnlyList registrations) { - var fallbackHandlerTypeMetadataNames = registrations - .Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName) - .Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName)) - .Distinct(StringComparer.Ordinal) - .Cast() - .ToImmutableArray(); + var fallbackCandidates = CollectFallbackCandidates(registrations); + if (fallbackCandidates.Count == 0) + return new ReflectionFallbackEmissionSpec(ImmutableArray.Empty); - if (fallbackHandlerTypeMetadataNames.IsDefaultOrEmpty) + var fallbackHandlerTypeMetadataNames = GetSortedFallbackMetadataNames(fallbackCandidates); + var fallbackHandlerTypeDisplayNames = GetSortedDirectFallbackDisplayNames(fallbackCandidates); + + if (TryCreateDirectFallbackEmission( + generationEnvironment, + fallbackHandlerTypeDisplayNames, + fallbackHandlerTypeMetadataNames, + out var directFallbackEmission)) { - return new ReflectionFallbackEmissionSpec( - EmitDirectTypeReferences: false, - HandlerTypeDisplayNames: ImmutableArray.Empty, - HandlerTypeMetadataNames: ImmutableArray.Empty); + return directFallbackEmission; } - var fallbackHandlerTypeDisplayNames = registrations - .Select(static registration => registration.ReflectionFallbackHandlerTypeDisplayName) - .Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName)) - .Distinct(StringComparer.Ordinal) - .Cast() - .ToImmutableArray(); + if (TryCreateMixedFallbackEmission( + generationEnvironment, + fallbackCandidates, + fallbackHandlerTypeDisplayNames, + out var mixedFallbackEmission)) + { + return mixedFallbackEmission; + } + return CreateNamedFallbackEmission(fallbackHandlerTypeMetadataNames); + } + + /// + /// 收集本轮所有 fallback handlers 的稳定元数据名和可选直接引用显示名。 + /// + private static Dictionary CollectFallbackCandidates( + IReadOnlyList registrations) + { + var fallbackCandidates = new Dictionary(StringComparer.Ordinal); + foreach (var registration in registrations) + { + if (string.IsNullOrWhiteSpace(registration.ReflectionFallbackHandlerTypeMetadataName)) + continue; + + fallbackCandidates[registration.ReflectionFallbackHandlerTypeMetadataName!] = + registration.ReflectionFallbackHandlerTypeDisplayName; + } + + return fallbackCandidates; + } + + /// + /// 获取按稳定顺序排列的 fallback handler 元数据名称集合。 + /// + private static ImmutableArray GetSortedFallbackMetadataNames( + IReadOnlyDictionary fallbackCandidates) + { + return fallbackCandidates.Keys + .OrderBy(static metadataName => metadataName, StringComparer.Ordinal) + .ToImmutableArray(); + } + + /// + /// 获取按稳定顺序排列的可直接引用 fallback handler 显示名集合。 + /// + private static ImmutableArray GetSortedDirectFallbackDisplayNames( + IReadOnlyDictionary fallbackCandidates) + { + return fallbackCandidates.Values + .Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName)) + .Cast() + .OrderBy(static typeDisplayName => typeDisplayName, StringComparer.Ordinal) + .ToImmutableArray(); + } + + /// + /// 当全部 fallback handlers 都可直接引用时,尝试创建直接 元数据发射策略。 + /// + private static bool TryCreateDirectFallbackEmission( + GenerationEnvironment generationEnvironment, + ImmutableArray fallbackHandlerTypeDisplayNames, + ImmutableArray fallbackHandlerTypeMetadataNames, + out ReflectionFallbackEmissionSpec emission) + { if (generationEnvironment.SupportsDirectReflectionFallbackTypes && fallbackHandlerTypeDisplayNames.Length == fallbackHandlerTypeMetadataNames.Length) { - return new ReflectionFallbackEmissionSpec( - EmitDirectTypeReferences: true, - HandlerTypeDisplayNames: fallbackHandlerTypeDisplayNames, - HandlerTypeMetadataNames: ImmutableArray.Empty); + emission = new ReflectionFallbackEmissionSpec( + [ + new ReflectionFallbackAttributeEmissionSpec( + EmitDirectTypeReferences: true, + fallbackHandlerTypeDisplayNames) + ]); + return true; } + emission = default; + return false; + } + + /// + /// 当 runtime 允许多个 fallback 特性实例时,尝试为 mixed 场景拆分直接 与字符串元数据。 + /// + private static bool TryCreateMixedFallbackEmission( + GenerationEnvironment generationEnvironment, + IReadOnlyDictionary fallbackCandidates, + ImmutableArray fallbackHandlerTypeDisplayNames, + out ReflectionFallbackEmissionSpec emission) + { + if (!generationEnvironment.SupportsDirectReflectionFallbackTypes || + !generationEnvironment.SupportsNamedReflectionFallbackTypes || + !generationEnvironment.SupportsMultipleReflectionFallbackAttributes || + fallbackHandlerTypeDisplayNames.Length == 0) + { + emission = default; + return false; + } + + var namedOnlyFallbackMetadataNames = fallbackCandidates + .Where(static pair => string.IsNullOrWhiteSpace(pair.Value)) + .Select(static pair => pair.Key) + .OrderBy(static metadataName => metadataName, StringComparer.Ordinal) + .ToImmutableArray(); + + if (namedOnlyFallbackMetadataNames.Length == 0) + { + emission = default; + return false; + } + + emission = new ReflectionFallbackEmissionSpec( + [ + new ReflectionFallbackAttributeEmissionSpec( + EmitDirectTypeReferences: true, + fallbackHandlerTypeDisplayNames), + new ReflectionFallbackAttributeEmissionSpec( + EmitDirectTypeReferences: false, + namedOnlyFallbackMetadataNames) + ]); + return true; + } + + /// + /// 创建统一的字符串 fallback 元数据发射策略。 + /// + private static ReflectionFallbackEmissionSpec CreateNamedFallbackEmission( + ImmutableArray fallbackHandlerTypeMetadataNames) + { return new ReflectionFallbackEmissionSpec( - EmitDirectTypeReferences: false, - HandlerTypeDisplayNames: ImmutableArray.Empty, - HandlerTypeMetadataNames: fallbackHandlerTypeMetadataNames); + [ + new ReflectionFallbackAttributeEmissionSpec( + EmitDirectTypeReferences: false, + fallbackHandlerTypeMetadataNames) + ]); } /// @@ -448,6 +580,36 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } + /// + /// 判断目标特性的 是否允许在同一程序集上声明多个实例。 + /// + private static bool SupportsMultipleAttributeInstances(INamedTypeSymbol attributeType) + { + foreach (var attribute in attributeType.GetAttributes()) + { + if (!string.Equals( + attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + "global::System.AttributeUsageAttribute", + StringComparison.Ordinal)) + { + continue; + } + + foreach (var namedArgument in attribute.NamedArguments) + { + if (string.Equals(namedArgument.Key, "AllowMultiple", StringComparison.Ordinal) && + namedArgument.Value.Value is bool allowMultiple) + { + return allowMultiple; + } + } + + return false; + } + + return false; + } + private static bool IsConcreteHandlerType(INamedTypeSymbol type) { return type.TypeKind is TypeKind.Class or TypeKind.Struct && diff --git a/GFramework.Cqrs.SourceGenerators/README.md b/GFramework.Cqrs.SourceGenerators/README.md index 420952b2..1a77b07a 100644 --- a/GFramework.Cqrs.SourceGenerators/README.md +++ b/GFramework.Cqrs.SourceGenerators/README.md @@ -33,7 +33,7 @@ - `Cqrs/CqrsHandlerRegistryGenerator.cs` 它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。 -当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据,进一步减少运行时按类型名回查程序集的成本。 +当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。 ## 最小接入路径 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 7e5afc7d..40a1da7b 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -341,6 +341,64 @@ internal sealed class CqrsHandlerRegistrarTests generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); } + /// + /// 验证当程序集同时声明直接 fallback 与字符串名称 fallback 时, + /// 运行时会优先复用直接类型,并只对名称 fallback 做定向 GetType(...) 查找。 + /// + [Test] + public void RegisterHandlers_Should_Use_Mixed_Fallback_Metadata_With_Targeted_Type_Lookups_Only_For_Named_Entries() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.Assembly.FullName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) + .Returns( + [ + new CqrsReflectionFallbackAttribute( + ReflectionFallbackNotificationContainer.DirectFallbackHandlerType), + new CqrsReflectionFallbackAttribute( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!) + ]); + generatedAssembly + .Setup(static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false)) + .Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var registrations = container.GetServicesUnsafe + .Where(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler) && + descriptor.ImplementationType is not null) + .Select(static descriptor => descriptor.ImplementationType!) + .ToList(); + + Assert.That( + registrations, + Is.EqualTo( + [ + typeof(GeneratedRegistryNotificationHandler), + ReflectionFallbackNotificationContainer.DirectFallbackHandlerType, + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + ])); + + generatedAssembly.Verify( + static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false), + Times.Once); + generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); + } + /// /// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据, /// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。 diff --git a/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs b/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs index b0475e01..c80465ce 100644 --- a/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs +++ b/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs @@ -10,11 +10,31 @@ namespace GFramework.Cqrs.Tests.Cqrs; /// internal sealed class ReflectionFallbackNotificationContainer { + /// + /// 获取可被直接引用、适合通过 元数据补扫的处理器类型。 + /// + public static Type DirectFallbackHandlerType => typeof(DirectFallbackGeneratedRegistryNotificationHandler); + /// /// 获取仅能通过反射补扫接入的私有嵌套处理器类型。 /// public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler); + private sealed class DirectFallbackGeneratedRegistryNotificationHandler + : INotificationHandler + { + /// + /// 处理测试通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + } + private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler : INotificationHandler { diff --git a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs index da557d84..b16afe0b 100644 --- a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs +++ b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs @@ -7,8 +7,10 @@ namespace GFramework.Cqrs; /// 该特性通常由源码生成器自动添加到消费端程序集。 /// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描, /// 以覆盖那些生成代码无法直接引用的 handler 类型。 +/// 允许同一程序集声明多个该特性实例,以便生成器把“可直接引用的 fallback handlers” +/// 和“仍需按名称恢复的 fallback handlers”拆成独立元数据块,进一步减少运行时字符串查找成本。 /// -[AttributeUsage(AttributeTargets.Assembly)] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public sealed class CqrsReflectionFallbackAttribute : Attribute { /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index d6204e64..8b5b1b56 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1446,6 +1446,89 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string AssemblyLevelMixedFallbackMetadataSource = """ + 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 { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + 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, AllowMultiple = true)] + 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>; + + private unsafe sealed record BetaRequest() : IRequest>; + + public unsafe sealed class AlphaHandler : IRequestHandler> + { + } + + private unsafe sealed class BetaHandler : IRequestHandler> + { + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -1812,6 +1895,45 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 允许多个 fallback 特性实例,且本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers 时, + /// 生成器会拆分出直接 与字符串两类元数据,避免 mixed 场景整体退回字符串 fallback。 + /// + [Test] + public void + Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes() + { + var execution = ExecuteGenerator( + AssemblyLevelMixedFallbackMetadataSource, + 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))]")); + Assert.That( + execution.GeneratedSources[0].content, + Does.Contain( + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// 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 5ac56983..e6905ac8 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,14 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-051` +- 恢复点编号:`CQRS-REWRITE-RP-052` - 当前阶段:`Phase 8` - 当前焦点: - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 + - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 + - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 - - mixed fallback 场景继续整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers + - 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时,mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers - 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke` - 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物 - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码 @@ -58,6 +60,12 @@ CQRS 迁移与收敛。 - 当本轮 fallback handlers 全部可被生成代码直接引用时,生成器会优先发射 `typeof(...)` 形式的程序集级 fallback 元数据,减少运行时 `Assembly.GetType(...)` 回查 - 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续统一发射字符串元数据,避免 mixed 场景只恢复部分 handlers - `GFramework.SourceGenerators.Tests` 已补充 runtime 同时暴露两类构造函数时优先选择直接 `Type` 元数据的回归 +- `2026-04-29` 已完成一轮 mixed fallback 元数据拆分: + - `CqrsReflectionFallbackAttribute` 现显式允许 `AllowMultiple = true` + - `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否允许多个 fallback 特性实例 + - 当本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 同时支持 `Type[]`、`string[]` 和多实例特性时,生成器会拆分输出两段 fallback 元数据 + - `GFramework.Cqrs.Tests` 已补充 mixed fallback metadata 回归,锁定 registrar 只对字符串条目执行定向 `Assembly.GetType(...)` + - `GFramework.SourceGenerators.Tests` 已补充 mixed fallback emission 回归,锁定 generator 会输出两个程序集级 fallback 特性实例而不是整体退回字符串 - 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 @@ -96,9 +104,21 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - 结果:通过 - 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - 结果:通过 + - 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径 ## 下一步 -1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍依赖程序集级 fallback 字符串元数据的场景 +1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍必须依赖字符串 fallback 元数据的 handler 类型形态 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 28f3a4e0..33aa44fa 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,38 @@ # CQRS 重写迁移追踪 +## 2026-04-29 + +### 阶段:mixed fallback 元数据拆分(CQRS-REWRITE-RP-052) + +- 延续 `gframework-batch-boot 50` 的 `Phase 8` 主线,本轮把上一批的“全部可直接引用 fallback handlers 走 `Type[]`”继续推进到 mixed 场景 +- 先复核现状后确认: + - `CqrsHandlerRegistrar` 已天然支持读取多个 `CqrsReflectionFallbackAttribute` 实例 + - 上一批真正阻止 mixed 场景继续收敛的点,是 runtime attribute 本身尚未开放多实例,以及 generator 只能二选一发射单个 fallback 特性 +- 已在 `GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs` 中将特性约束改为 `AllowMultiple = true`,并补充注释说明多个实例的用途 +- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中扩展 fallback 合同探测: + - 探测 runtime 是否支持 `params string[]` + - 探测 runtime 是否支持 `params Type[]` + - 探测 runtime 是否允许多个 `CqrsReflectionFallbackAttribute` 实例 +- 已在 `CqrsHandlerRegistryGenerator.Models.cs` 与 `CqrsHandlerRegistryGenerator.SourceEmission.cs` 中重构 fallback 发射模型: + - fallback 元数据现在可表示为一个或多个程序集级特性实例 + - 当 fallback handlers 全部可直接引用时,继续优先输出单个 `Type[]` 特性 + - 当 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 支持多实例时,拆分输出一条 `Type[]` 特性和一条字符串特性 + - 若 runtime 不支持多实例或缺少相应构造函数,仍整体回退到字符串元数据,避免 mixed 场景漏注册 +- 已补充 runtime 与 generator 双侧回归: + - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 新增 mixed fallback metadata 用例,锁定 registrar 只对字符串条目调用一次 `Assembly.GetType(...)` + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增 mixed fallback emission 用例,锁定 generator 会输出两个程序集级 fallback 特性实例 +- 同步更新: + - `GFramework.Cqrs.SourceGenerators/README.md` + - `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` + - 说明 mixed 场景现在会拆分 `Type` 元数据与字符串元数据 +- 定向验证已通过: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - `13/13` passed + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` + - `18/18` passed + ## 2026-04-20 ### 阶段:direct fallback 元数据优先级收敛(CQRS-REWRITE-RP-051) diff --git a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md index 4366fedf..21436faa 100644 --- a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md +++ b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md @@ -32,7 +32,7 @@ runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整 - 程序集级 `CqrsReflectionFallbackAttribute` 这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。 -如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据,进一步减少 runtime 再做字符串类型名回查的成本。 +如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。 ## 最小接入路径 @@ -109,7 +109,9 @@ RegisterCqrsHandlersFromAssemblies( - 能直接引用的 handler,生成直接注册语句 - 实现类型不能直接引用、但服务接口还能精确表达时,生成反射实现类型查找 - 服务接口本身也需要运行时解析时,生成精确 type lookup -- 当 fallback handlers 全部可直接引用且 runtime 暴露 `params Type[]` 合同时,优先发射直接 `Type` 元数据;否则统一回退到字符串元数据,避免 mixed 场景漏注册 +- 当 fallback handlers 全部可直接引用且 runtime 暴露 `params Type[]` 合同时,优先发射直接 `Type` 元数据 +- 当 mixed 场景同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 允许多个 fallback 特性实例时,拆分发射 `Type` 元数据和字符串元数据 +- 其余场景统一回退到字符串元数据,避免 mixed 场景漏注册 - 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果 如果当前编译环境缺少这个 fallback 合同,而某些 handler 又必须依赖它,生成器会报: From 8d8b94f60886698ec97ad34eac72aa7038f6acfe Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:20:15 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=20fallback?= =?UTF-8?q?=20=E5=AE=A1=E6=9F=A5=E8=B7=9F=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 generator preamble 的多实例 fallback 特性排版并移除死参数 - 补强 mixed/direct fallback 生成回归断言并拒绝空 marker - 更新 CQRS 审查跟踪记录与 XML 文档 --- ...HandlerRegistryGenerator.SourceEmission.cs | 20 +++--- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 2 +- ...ReflectionFallbackNotificationContainer.cs | 3 + .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 65 +++++++++++++++++-- .../todos/cqrs-rewrite-migration-tracking.md | 17 +++++ .../traces/cqrs-rewrite-migration-trace.md | 16 +++++ 6 files changed, 107 insertions(+), 16 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 3391e6ad..4db622cb 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -8,15 +8,12 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 生成程序集级 CQRS handler 注册器源码。 /// - /// - /// 当前轮次的生成环境,用于决定 runtime 是否提供 CqrsReflectionFallbackAttribute 契约,以及是否需要在输出中发射对应的程序集级元数据。 - /// /// /// 已整理并排序的 handler 注册描述。方法会据此生成 CqrsHandlerRegistry.g.cs,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。 /// /// /// 当前轮次选定的程序集级 reflection fallback 元数据发射策略。 - /// 调用方必须先确保:若该策略包含 fallback handlers,则 已声明支持对应的 fallback attribute 契约; + /// 调用方必须先确保:若该策略包含 fallback handlers,则当前 runtime 已声明支持对应的 fallback attribute 契约; /// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。 /// /// 完整的注册器源代码文本。 @@ -28,13 +25,12 @@ public sealed partial class CqrsHandlerRegistryGenerator /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 /// private static string GenerateSource( - GenerationEnvironment generationEnvironment, IReadOnlyList registrations, ReflectionFallbackEmissionSpec reflectionFallbackEmission) { var sourceShape = CreateGeneratedRegistrySourceShape(registrations); var builder = new StringBuilder(); - AppendGeneratedSourcePreamble(builder, generationEnvironment, reflectionFallbackEmission); + AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission); AppendGeneratedRegistryType(builder, registrations, sourceShape); return builder.ToString(); } @@ -68,11 +64,9 @@ public sealed partial class CqrsHandlerRegistryGenerator /// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。 /// /// 生成源码构造器。 - /// 当前轮次的生成环境。 /// 需要写入程序集级 reflection fallback 特性的元数据策略。 private static void AppendGeneratedSourcePreamble( StringBuilder builder, - GenerationEnvironment generationEnvironment, ReflectionFallbackEmissionSpec reflectionFallbackEmission) { builder.AppendLine("// "); @@ -102,10 +96,14 @@ public sealed partial class CqrsHandlerRegistryGenerator StringBuilder builder, ReflectionFallbackEmissionSpec reflectionFallbackEmission) { - foreach (var attributeEmission in reflectionFallbackEmission.Attributes) + for (var index = 0; index < reflectionFallbackEmission.Attributes.Length; index++) { - AppendReflectionFallbackAttribute(builder, attributeEmission); - builder.AppendLine(); + if (index > 0) + { + builder.AppendLine(); + } + + AppendReflectionFallbackAttribute(builder, reflectionFallbackEmission.Attributes[index]); } } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 688fbc9a..5ebe31b0 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -296,7 +296,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator context.AddSource( HintName, - GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission)); + GenerateSource(registrations, reflectionFallbackEmission)); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs b/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs index c80465ce..d54f0dfd 100644 --- a/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs +++ b/GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs @@ -13,6 +13,9 @@ internal sealed class ReflectionFallbackNotificationContainer /// /// 获取可被直接引用、适合通过 元数据补扫的处理器类型。 /// + /// + /// 可被生成注册器直接引用的 fallback 处理器类型,用于验证 runtime 会优先消费 元数据。 + /// public static Type DirectFallbackHandlerType => typeof(DirectFallbackGeneratedRegistryNotificationHandler); /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 8b5b1b56..78441c5b 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1876,6 +1876,7 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); + var generatedSource = execution.GeneratedSources[0].content; Assert.Multiple(() => { @@ -1885,13 +1886,24 @@ public class CqrsHandlerRegistryGeneratorTests 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, + generatedSource, Does.Contain( "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler), typeof(global::TestApp.Container.BetaHandler))]")); Assert.That( - execution.GeneratedSources[0].content, + generatedSource, Does.Not.Contain( "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+AlphaHandler\", \"TestApp.Container+BetaHandler\")]")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute()")); + Assert.That( + CountOccurrences( + generatedSource, + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute"), + Is.EqualTo(1)); + Assert.That( + CountOccurrences( + generatedSource, + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\""), + Is.Zero); }); } @@ -1915,6 +1927,7 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); + var generatedSource = execution.GeneratedSources[0].content; Assert.Multiple(() => { @@ -1924,13 +1937,32 @@ public class CqrsHandlerRegistryGeneratorTests 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, + generatedSource, Does.Contain( "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler))]")); Assert.That( - execution.GeneratedSources[0].content, + generatedSource, Does.Contain( "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute()")); + Assert.That( + CountOccurrences( + generatedSource, + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute"), + Is.EqualTo(2)); + Assert.That( + CountOccurrences( + generatedSource, + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\""), + Is.EqualTo(1)); + Assert.That( + generatedSource, + Does.Contain( + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler))]" + + Environment.NewLine + + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]" + + Environment.NewLine + + "[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]")); }); } @@ -1985,6 +2017,31 @@ public class CqrsHandlerRegistryGeneratorTests return execution.GeneratedSources[0].content; } + /// + /// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。 + /// + /// 待统计的完整生成源码。 + /// 需要计数的固定片段。 + /// 中出现的次数。 + private static int CountOccurrences(string text, string value) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException("The search value must not be null or empty.", nameof(value)); + + var count = 0; + var startIndex = 0; + + while (true) + { + var nextIndex = text.IndexOf(value, startIndex, global::System.StringComparison.Ordinal); + if (nextIndex < 0) + return count; + + count++; + startIndex = nextIndex + value.Length; + } + } + /// /// 运行 CQRS handler registry generator,并返回生成输出及相关诊断。 /// 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 e6905ac8..64365852 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 @@ -66,6 +66,11 @@ CQRS 迁移与收敛。 - 当本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers,且 runtime 同时支持 `Type[]`、`string[]` 和多实例特性时,生成器会拆分输出两段 fallback 元数据 - `GFramework.Cqrs.Tests` 已补充 mixed fallback metadata 回归,锁定 registrar 只对字符串条目执行定向 `Assembly.GetType(...)` - `GFramework.SourceGenerators.Tests` 已补充 mixed fallback emission 回归,锁定 generator 会输出两个程序集级 fallback 特性实例而不是整体退回字符串 +- `2026-04-29` 已重新执行 `$gframework-pr-review`: + - 当前分支对应 `PR #302`,状态为 `OPEN` + - latest reviewed commit 当前剩余 `3` 条 open AI review threads:`2` 条 Greptile、`1` 条 CodeRabbit + - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 + - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 - 当前主线优先级: - generator 覆盖面继续扩大 - dispatch/invoker 反射占比继续下降 @@ -116,6 +121,18 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - 结果:通过 - 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径 +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #302`;latest head review 仍有 `3` 条 open AI threads,其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音 +- `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"` + - 结果:通过 + - 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - 结果:通过 + - 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归 ## 下一步 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 33aa44fa..cb008db1 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 @@ -32,6 +32,22 @@ - `13/13` passed - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"` - `18/18` passed +- 随后按 `$gframework-pr-review` 重新拉取当前分支 PR 审查数据: + - 当前 worktree `feat/cqrs-optimization` 已对应 `PR #302` + - latest head commit 仍有 `3` 条 open AI review threads:Greptile 指向 generator preamble 的死参数与多实例 fallback 特性空行,CodeRabbit 指向 mixed/direct fallback 测试断言过宽 + - MegaLinter 仍只暴露 `dotnet-format` 的 `Restore operation failed`,未给出本地仍成立的格式文件线索,因此按环境噪音处理 +- 本轮已继续收口 `RP-052` 的 follow-up: + - 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 中移除已不再参与判断的 `generationEnvironment` 透传参数 + - 调整多实例 fallback 特性发射时的换行策略,避免最后一个 fallback 特性与 `CqrsHandlerRegistryAttribute` 之间保留多余空行 + - 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补强 direct/mixed fallback 发射断言,锁定特性实例个数、拒绝空 marker,并确保 mixed 场景的程序集级 preamble 排版稳定 + - 在 `GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs` 中为 `DirectFallbackHandlerType` 补齐 `` XML 文档 +- 本轮 review follow-up 验证已通过: + - `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"` + - `18/18` passed + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"` + - `13/13` passed ## 2026-04-20