perf(cqrs): 拆分混合 fallback 元数据

- 优化 CqrsReflectionFallbackAttribute 与生成器发射策略,在 mixed 场景下拆分 Type 与字符串 fallback 元数据
- 补充 CQRS runtime 与 SourceGenerators 回归测试,锁定多实例 fallback 特性和定向类型回查行为
- 更新 CQRS 生成器文档与 ai-plan 恢复记录,沉淀 RP-052 的验证结果与下一步
This commit is contained in:
gewuyou 2026-04-29 13:37:05 +08:00
parent 5fd71f3620
commit 76fcdb8233
11 changed files with 492 additions and 59 deletions

View File

@ -155,23 +155,26 @@ public sealed partial class CqrsHandlerRegistryGenerator
ImmutableArray<RuntimeTypeReferenceSpec> ServiceTypeArguments);
/// <summary>
/// 描述本轮生成应如何发射程序集级 reflection fallback 元数据
/// 描述单个程序集级 reflection fallback 特性实例的发射内容
/// </summary>
/// <remarks>
/// 生成器会优先尝试使用 <c>typeof(...)</c> 形式的 <see cref="Type" /> 元数据,
/// 以减少运行时再做字符串类型名回查的成本;但当任一 fallback handler 仍无法被生成代码直接引用时,
/// 会整体回退到字符串元数据,避免 mixed 场景下遗漏剩余 handler。
/// 某些运行时合同允许生成器把可直接引用的 fallback handlers 与必须按名称恢复的 handlers
/// 拆成多个特性实例,以进一步减少运行时字符串查找成本。
/// </remarks>
private readonly record struct ReflectionFallbackEmissionSpec(
private readonly record struct ReflectionFallbackAttributeEmissionSpec(
bool EmitDirectTypeReferences,
ImmutableArray<string> HandlerTypeDisplayNames,
ImmutableArray<string> HandlerTypeMetadataNames)
ImmutableArray<string> Values);
/// <summary>
/// 描述本轮生成应如何发射程序集级 reflection fallback 元数据。
/// </summary>
private readonly record struct ReflectionFallbackEmissionSpec(
ImmutableArray<ReflectionFallbackAttributeEmissionSpec> Attributes)
{
/// <summary>
/// 获取当前是否需要发射任何 fallback 元数据。
/// </summary>
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);
}

View File

@ -23,7 +23,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// <remarks>
/// 当 <paramref name="reflectionFallbackEmission" /> 不包含任何 fallback handlers 时,
/// 输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
/// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带程序集级
/// 当其包含 fallback handlers 且 runtime 合同可用时,输出还会附带一个或多个程序集级
/// <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
/// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
/// </remarks>
@ -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
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="reflectionFallbackEmission">需要写入特性的 fallback 元数据策略。</param>
private static void AppendReflectionFallbackAttribute(
private static void AppendReflectionFallbackAttributes(
StringBuilder builder,
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
foreach (var attributeEmission in reflectionFallbackEmission.Attributes)
{
AppendReflectionFallbackAttribute(builder, attributeEmission);
builder.AppendLine();
}
}
/// <summary>
/// 发射单个程序集级 reflection fallback 元数据特性实例。
/// </summary>
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(")]");
}
/// <summary>

View File

@ -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;
}
/// <summary>
@ -380,48 +396,164 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
/// 选择本轮生成应采用的 fallback 元数据发射策略。
/// </summary>
/// <remarks>
/// 只有当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 <c>params Type[]</c> 重载时,
/// 才会选择直接 <see cref="Type" /> 元数据;否则统一回退到字符串类型名,避免 mixed 场景丢失剩余 handler。
/// 当所有 fallback handlers 都能被生成代码直接引用,且 runtime 暴露了 <c>params Type[]</c> 重载时,
/// 优先输出单个直接 <see cref="Type" /> 元数据特性;当 runtime 同时支持多个特性实例时,
/// mixed 场景会拆分成“直接 <see cref="Type" /> + 字符串类型名”两类特性;其余场景统一回退到字符串元数据。
/// </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();
var fallbackCandidates = CollectFallbackCandidates(registrations);
if (fallbackCandidates.Count == 0)
return new ReflectionFallbackEmissionSpec(ImmutableArray<ReflectionFallbackAttributeEmissionSpec>.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<string>.Empty,
HandlerTypeMetadataNames: ImmutableArray<string>.Empty);
return directFallbackEmission;
}
var fallbackHandlerTypeDisplayNames = registrations
.Select(static registration => registration.ReflectionFallbackHandlerTypeDisplayName)
.Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName))
.Distinct(StringComparer.Ordinal)
.Cast<string>()
.ToImmutableArray();
if (TryCreateMixedFallbackEmission(
generationEnvironment,
fallbackCandidates,
fallbackHandlerTypeDisplayNames,
out var mixedFallbackEmission))
{
return mixedFallbackEmission;
}
return CreateNamedFallbackEmission(fallbackHandlerTypeMetadataNames);
}
/// <summary>
/// 收集本轮所有 fallback handlers 的稳定元数据名和可选直接引用显示名。
/// </summary>
private static Dictionary<string, string?> CollectFallbackCandidates(
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
var fallbackCandidates = new Dictionary<string, string?>(StringComparer.Ordinal);
foreach (var registration in registrations)
{
if (string.IsNullOrWhiteSpace(registration.ReflectionFallbackHandlerTypeMetadataName))
continue;
fallbackCandidates[registration.ReflectionFallbackHandlerTypeMetadataName!] =
registration.ReflectionFallbackHandlerTypeDisplayName;
}
return fallbackCandidates;
}
/// <summary>
/// 获取按稳定顺序排列的 fallback handler 元数据名称集合。
/// </summary>
private static ImmutableArray<string> GetSortedFallbackMetadataNames(
IReadOnlyDictionary<string, string?> fallbackCandidates)
{
return fallbackCandidates.Keys
.OrderBy(static metadataName => metadataName, StringComparer.Ordinal)
.ToImmutableArray();
}
/// <summary>
/// 获取按稳定顺序排列的可直接引用 fallback handler 显示名集合。
/// </summary>
private static ImmutableArray<string> GetSortedDirectFallbackDisplayNames(
IReadOnlyDictionary<string, string?> fallbackCandidates)
{
return fallbackCandidates.Values
.Where(static typeDisplayName => !string.IsNullOrWhiteSpace(typeDisplayName))
.Cast<string>()
.OrderBy(static typeDisplayName => typeDisplayName, StringComparer.Ordinal)
.ToImmutableArray();
}
/// <summary>
/// 当全部 fallback handlers 都可直接引用时,尝试创建直接 <see cref="Type" /> 元数据发射策略。
/// </summary>
private static bool TryCreateDirectFallbackEmission(
GenerationEnvironment generationEnvironment,
ImmutableArray<string> fallbackHandlerTypeDisplayNames,
ImmutableArray<string> fallbackHandlerTypeMetadataNames,
out ReflectionFallbackEmissionSpec emission)
{
if (generationEnvironment.SupportsDirectReflectionFallbackTypes &&
fallbackHandlerTypeDisplayNames.Length == fallbackHandlerTypeMetadataNames.Length)
{
return new ReflectionFallbackEmissionSpec(
emission = new ReflectionFallbackEmissionSpec(
[
new ReflectionFallbackAttributeEmissionSpec(
EmitDirectTypeReferences: true,
HandlerTypeDisplayNames: fallbackHandlerTypeDisplayNames,
HandlerTypeMetadataNames: ImmutableArray<string>.Empty);
fallbackHandlerTypeDisplayNames)
]);
return true;
}
return new ReflectionFallbackEmissionSpec(
emission = default;
return false;
}
/// <summary>
/// 当 runtime 允许多个 fallback 特性实例时,尝试为 mixed 场景拆分直接 <see cref="Type" /> 与字符串元数据。
/// </summary>
private static bool TryCreateMixedFallbackEmission(
GenerationEnvironment generationEnvironment,
IReadOnlyDictionary<string, string?> fallbackCandidates,
ImmutableArray<string> 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,
HandlerTypeDisplayNames: ImmutableArray<string>.Empty,
HandlerTypeMetadataNames: fallbackHandlerTypeMetadataNames);
namedOnlyFallbackMetadataNames)
]);
return true;
}
/// <summary>
/// 创建统一的字符串 fallback 元数据发射策略。
/// </summary>
private static ReflectionFallbackEmissionSpec CreateNamedFallbackEmission(
ImmutableArray<string> fallbackHandlerTypeMetadataNames)
{
return new ReflectionFallbackEmissionSpec(
[
new ReflectionFallbackAttributeEmissionSpec(
EmitDirectTypeReferences: false,
fallbackHandlerTypeMetadataNames)
]);
}
/// <summary>
@ -448,6 +580,36 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return false;
}
/// <summary>
/// 判断目标特性的 <see cref="AttributeUsageAttribute" /> 是否允许在同一程序集上声明多个实例。
/// </summary>
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 &&

View File

@ -33,7 +33,7 @@
- `Cqrs/CqrsHandlerRegistryGenerator.cs`
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据,进一步减少运行时按类型名回查程序集的成本。
当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。
## 最小接入路径

View File

@ -341,6 +341,64 @@ internal sealed class CqrsHandlerRegistrarTests
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
}
/// <summary>
/// 验证当程序集同时声明直接 <see cref="Type" /> fallback 与字符串名称 fallback 时,
/// 运行时会优先复用直接类型,并只对名称 fallback 做定向 <c>GetType(...)</c> 查找。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Mixed_Fallback_Metadata_With_Targeted_Type_Lookups_Only_For_Named_Entries()
{
var generatedAssembly = new Mock<Assembly>();
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<GeneratedRegistryNotification>) &&
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);
}
/// <summary>
/// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据,
/// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。

View File

@ -10,11 +10,31 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// </summary>
internal sealed class ReflectionFallbackNotificationContainer
{
/// <summary>
/// 获取可被直接引用、适合通过 <see cref="Type" /> 元数据补扫的处理器类型。
/// </summary>
public static Type DirectFallbackHandlerType => typeof(DirectFallbackGeneratedRegistryNotificationHandler);
/// <summary>
/// 获取仅能通过反射补扫接入的私有嵌套处理器类型。
/// </summary>
public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler);
private sealed class DirectFallbackGeneratedRegistryNotificationHandler
: INotificationHandler<GeneratedRegistryNotification>
{
/// <summary>
/// 处理测试通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler
: INotificationHandler<GeneratedRegistryNotification>
{

View File

@ -7,8 +7,10 @@ namespace GFramework.Cqrs;
/// 该特性通常由源码生成器自动添加到消费端程序集。
/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描,
/// 以覆盖那些生成代码无法直接引用的 handler 类型。
/// 允许同一程序集声明多个该特性实例,以便生成器把“可直接引用的 fallback handlers”
/// 和“仍需按名称恢复的 fallback handlers”拆成独立元数据块进一步减少运行时字符串查找成本。
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly)]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
/// <summary>

View File

@ -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<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, 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<delegate* unmanaged<AlphaResponse>>;
private unsafe sealed record BetaRequest() : IRequest<delegate* unmanaged<BetaResponse>>;
public unsafe sealed class AlphaHandler : IRequestHandler<AlphaRequest, delegate* unmanaged<AlphaResponse>>
{
}
private unsafe sealed class BetaHandler : IRequestHandler<BetaRequest, delegate* unmanaged<BetaResponse>>
{
}
}
}
""";
/// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary>
@ -1812,6 +1895,45 @@ public class CqrsHandlerRegistryGeneratorTests
});
}
/// <summary>
/// 验证当 runtime 允许多个 fallback 特性实例,且本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers 时,
/// 生成器会拆分出直接 <see cref="Type" /> 与字符串两类元数据,避免 mixed 场景整体退回字符串 fallback。
/// </summary>
[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\")]"));
});
}
/// <summary>
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
/// </summary>

View File

@ -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` 作为独立验证步骤

View File

@ -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

View File

@ -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 又必须依赖它,生成器会报: