feat(cqrs): 前移请求调用器生成注册

- 新增 generated request invoker provider seam,并让 registrar 与 dispatcher 复用编译期请求调用元数据

- 扩展 CQRS source generator 发射 request invoker provider 成员与最小 request invoker 方法

- 补充 runtime 与 source-generator 回归测试,并更新 cqrs-rewrite 追踪到 RP-067
This commit is contained in:
gewuyou 2026-04-30 12:10:25 +08:00
parent 7209fdc32d
commit 0c65cd8e38
16 changed files with 956 additions and 11 deletions

View File

@ -5,11 +5,16 @@ namespace GFramework.Cqrs.SourceGenerators.Cqrs;
/// </summary> /// </summary>
public sealed partial class CqrsHandlerRegistryGenerator public sealed partial class CqrsHandlerRegistryGenerator
{ {
private readonly record struct RequestInvokerRegistrationSpec(
string RequestTypeDisplayName,
string ResponseTypeDisplayName);
private readonly record struct HandlerRegistrationSpec( private readonly record struct HandlerRegistrationSpec(
string HandlerInterfaceDisplayName, string HandlerInterfaceDisplayName,
string ImplementationTypeDisplayName, string ImplementationTypeDisplayName,
string HandlerInterfaceLogName, string HandlerInterfaceLogName,
string ImplementationLogName); string ImplementationLogName,
RequestInvokerRegistrationSpec? RequestInvokerRegistration);
private readonly record struct ReflectedImplementationRegistrationSpec( private readonly record struct ReflectedImplementationRegistrationSpec(
string HandlerInterfaceDisplayName, string HandlerInterfaceDisplayName,
@ -24,14 +29,24 @@ public sealed partial class CqrsHandlerRegistryGenerator
bool HasReflectedImplementationRegistrations, bool HasReflectedImplementationRegistrations,
bool HasPreciseReflectedRegistrations, bool HasPreciseReflectedRegistrations,
bool HasReflectionTypeLookups, bool HasReflectionTypeLookups,
bool HasExternalAssemblyTypeLookups) bool HasExternalAssemblyTypeLookups,
bool SupportsRequestInvokerProvider,
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions)
{ {
public bool RequiresRegistryAssemblyVariable => public bool RequiresRegistryAssemblyVariable =>
HasReflectedImplementationRegistrations || HasReflectedImplementationRegistrations ||
HasPreciseReflectedRegistrations || HasPreciseReflectedRegistrations ||
HasReflectionTypeLookups; HasReflectionTypeLookups;
public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty;
} }
private readonly record struct RequestInvokerEmissionSpec(
string RequestTypeDisplayName,
string ResponseTypeDisplayName,
string HandlerInterfaceDisplayName,
int MethodIndex);
/// <summary> /// <summary>
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。 /// 标记某条 handler 注册语句在生成阶段采用的表达策略。
/// </summary> /// </summary>
@ -312,5 +327,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
bool GenerationEnabled, bool GenerationEnabled,
bool SupportsNamedReflectionFallbackTypes, bool SupportsNamedReflectionFallbackTypes,
bool SupportsDirectReflectionFallbackTypes, bool SupportsDirectReflectionFallbackTypes,
bool SupportsMultipleReflectionFallbackAttributes); bool SupportsMultipleReflectionFallbackAttributes,
bool SupportsRequestInvokerProvider);
} }

View File

@ -25,10 +25,11 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 /// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
/// </remarks> /// </remarks>
private static string GenerateSource( private static string GenerateSource(
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations, IReadOnlyList<ImplementationRegistrationSpec> registrations,
ReflectionFallbackEmissionSpec reflectionFallbackEmission) ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{ {
var sourceShape = CreateGeneratedRegistrySourceShape(registrations); var sourceShape = CreateGeneratedRegistrySourceShape(generationEnvironment, registrations);
var builder = new StringBuilder(); var builder = new StringBuilder();
AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission); AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission);
AppendGeneratedRegistryType(builder, registrations, sourceShape); AppendGeneratedRegistryType(builder, registrations, sourceShape);
@ -41,6 +42,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// <param name="registrations">已整理并排序的 handler 注册描述。</param> /// <param name="registrations">已整理并排序的 handler 注册描述。</param>
/// <returns>当前生成输出需要启用的结构分支。</returns> /// <returns>当前生成输出需要启用的结构分支。</returns>
private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape( private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape(
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations) IReadOnlyList<ImplementationRegistrationSpec> registrations)
{ {
var hasReflectedImplementationRegistrations = registrations.Any(static registration => var hasReflectedImplementationRegistrations = registrations.Any(static registration =>
@ -52,12 +54,44 @@ public sealed partial class CqrsHandlerRegistryGenerator
var hasExternalAssemblyTypeLookups = registrations.Any(static registration => var hasExternalAssemblyTypeLookups = registrations.Any(static registration =>
registration.PreciseReflectedRegistrations.Any(static preciseRegistration => registration.PreciseReflectedRegistrations.Any(static preciseRegistration =>
preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup)));
var requestInvokerEmissions = CreateRequestInvokerEmissions(
generationEnvironment.SupportsRequestInvokerProvider,
registrations);
return new GeneratedRegistrySourceShape( return new GeneratedRegistrySourceShape(
hasReflectedImplementationRegistrations, hasReflectedImplementationRegistrations,
hasPreciseReflectedRegistrations, hasPreciseReflectedRegistrations,
hasReflectionTypeLookups, hasReflectionTypeLookups,
hasExternalAssemblyTypeLookups); hasExternalAssemblyTypeLookups,
generationEnvironment.SupportsRequestInvokerProvider,
requestInvokerEmissions);
}
private static ImmutableArray<RequestInvokerEmissionSpec> CreateRequestInvokerEmissions(
bool supportsRequestInvokerProvider,
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
if (!supportsRequestInvokerProvider)
return ImmutableArray<RequestInvokerEmissionSpec>.Empty;
var builder = ImmutableArray.CreateBuilder<RequestInvokerEmissionSpec>();
var methodIndex = 0;
foreach (var registration in registrations)
{
foreach (var directRegistration in registration.DirectRegistrations)
{
if (directRegistration.RequestInvokerRegistration is not { } requestInvokerRegistration)
continue;
builder.Add(new RequestInvokerEmissionSpec(
requestInvokerRegistration.RequestTypeDisplayName,
requestInvokerRegistration.ResponseTypeDisplayName,
directRegistration.HandlerInterfaceDisplayName,
methodIndex++));
}
}
return builder.ToImmutable();
} }
/// <summary> /// <summary>
@ -160,10 +194,26 @@ public sealed partial class CqrsHandlerRegistryGenerator
builder.Append(GeneratedTypeName); builder.Append(GeneratedTypeName);
builder.Append(" : global::"); builder.Append(" : global::");
builder.Append(CqrsRuntimeNamespace); builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".ICqrsHandlerRegistry"); builder.Append(".ICqrsHandlerRegistry");
if (sourceShape.HasRequestInvokerProvider)
{
builder.Append(", global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".ICqrsRequestInvokerProvider, global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors");
}
builder.AppendLine();
builder.AppendLine("{"); builder.AppendLine("{");
AppendRegisterMethod(builder, registrations, sourceShape); AppendRegisterMethod(builder, registrations, sourceShape);
if (sourceShape.HasRequestInvokerProvider)
{
builder.AppendLine();
AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions);
}
if (sourceShape.HasExternalAssemblyTypeLookups) if (sourceShape.HasExternalAssemblyTypeLookups)
{ {
builder.AppendLine(); builder.AppendLine();
@ -223,6 +273,103 @@ public sealed partial class CqrsHandlerRegistryGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
} }
private static void AppendRequestInvokerProviderMembers(
StringBuilder builder,
ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions)
{
AppendRequestInvokerDescriptorArray(builder, requestInvokerEmissions);
builder.AppendLine();
AppendRequestInvokerProviderMethods(builder);
for (var index = 0; index < requestInvokerEmissions.Length; index++)
{
builder.AppendLine();
AppendRequestInvokerMethod(builder, requestInvokerEmissions[index]);
}
}
private static void AppendRequestInvokerDescriptorArray(
StringBuilder builder,
ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions)
{
builder.AppendLine(" private static readonly global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry[] RequestInvokerDescriptors =");
builder.AppendLine(" [");
for (var index = 0; index < requestInvokerEmissions.Length; index++)
{
var emission = requestInvokerEmissions[index];
builder.Append(" new global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsRequestInvokerDescriptorEntry(typeof(");
builder.Append(emission.RequestTypeDisplayName);
builder.Append("), typeof(");
builder.Append(emission.ResponseTypeDisplayName);
builder.Append("), new global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsRequestInvokerDescriptor(typeof(");
builder.Append(emission.HandlerInterfaceDisplayName);
builder.Append("), typeof(");
builder.Append(GeneratedTypeName);
builder.Append(").GetMethod(nameof(InvokeRequestHandler");
builder.Append(emission.MethodIndex);
builder.Append("), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))");
builder.AppendLine(index == requestInvokerEmissions.Length - 1 ? string.Empty : ",");
}
builder.AppendLine(" ];");
}
private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
{
builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::");
builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".CqrsRequestInvokerDescriptorEntry> GetDescriptors()");
builder.AppendLine(" {");
builder.AppendLine(" return RequestInvokerDescriptors;");
builder.AppendLine(" }");
builder.AppendLine();
builder.Append(" public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::");
builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".CqrsRequestInvokerDescriptor? descriptor)");
builder.AppendLine(" {");
builder.AppendLine(" if (requestType is null)");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(requestType));");
builder.AppendLine(" if (responseType is null)");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(responseType));");
builder.AppendLine();
builder.AppendLine(" foreach (var entry in RequestInvokerDescriptors)");
builder.AppendLine(" {");
builder.AppendLine(" if (entry.RequestType == requestType && entry.ResponseType == responseType)");
builder.AppendLine(" {");
builder.AppendLine(" descriptor = entry.Descriptor;");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" descriptor = null;");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
}
private static void AppendRequestInvokerMethod(StringBuilder builder, RequestInvokerEmissionSpec emission)
{
builder.Append(" private static global::System.Threading.Tasks.ValueTask<");
builder.Append(emission.ResponseTypeDisplayName);
builder.Append("> InvokeRequestHandler");
builder.Append(emission.MethodIndex);
builder.Append("(object handler, object request, global::System.Threading.CancellationToken cancellationToken)");
builder.AppendLine();
builder.AppendLine(" {");
builder.Append(" var typedHandler = (");
builder.Append(emission.HandlerInterfaceDisplayName);
builder.AppendLine(")handler;");
builder.Append(" var typedRequest = (");
builder.Append(emission.RequestTypeDisplayName);
builder.AppendLine(")request;");
builder.AppendLine(" return typedHandler.Handle(typedRequest, cancellationToken);");
builder.AppendLine(" }");
}
private static void AppendDirectRegistrations( private static void AppendDirectRegistrations(
StringBuilder builder, StringBuilder builder,
ImplementationRegistrationSpec registration) ImplementationRegistrationSpec registration)

View File

@ -15,6 +15,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1"; private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1";
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
private const string ICqrsRequestInvokerProviderMetadataName = $"{CqrsRuntimeNamespace}.ICqrsRequestInvokerProvider";
private const string IEnumeratesCqrsRequestInvokerDescriptorsMetadataName =
$"{CqrsRuntimeNamespace}.IEnumeratesCqrsRequestInvokerDescriptors";
private const string CqrsRequestInvokerDescriptorMetadataName =
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor";
private const string CqrsRequestInvokerDescriptorEntryMetadataName =
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry";
private const string CqrsHandlerRegistryAttributeMetadataName = private const string CqrsHandlerRegistryAttributeMetadataName =
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
@ -66,6 +73,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
CqrsHandlerRegistryAttributeMetadataName) is not null && CqrsHandlerRegistryAttributeMetadataName) is not null &&
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
var supportsRequestInvokerProvider =
compilation.GetTypeByMetadataName(ICqrsRequestInvokerProviderMetadataName) is not null &&
compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null &&
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null &&
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) is not null;
var stringType = compilation.GetSpecialType(SpecialType.System_String); var stringType = compilation.GetSpecialType(SpecialType.System_String);
var typeType = compilation.GetTypeByMetadataName("System.Type"); var typeType = compilation.GetTypeByMetadataName("System.Type");
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null && var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
@ -85,7 +97,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
generationEnabled, generationEnabled,
supportsNamedReflectionFallbackTypes, supportsNamedReflectionFallbackTypes,
supportsDirectReflectionFallbackTypes, supportsDirectReflectionFallbackTypes,
supportsMultipleReflectionFallbackAttributes); supportsMultipleReflectionFallbackAttributes,
supportsRequestInvokerProvider);
} }
private static bool IsHandlerCandidate(SyntaxNode node) private static bool IsHandlerCandidate(SyntaxNode node)
@ -218,7 +231,10 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
implementationTypeDisplayName, implementationTypeDisplayName,
GetLogDisplayName(handlerInterface), GetLogDisplayName(handlerInterface),
implementationLogName)); implementationLogName,
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
? requestInvokerRegistration
: null));
return true; return true;
} }
@ -237,6 +253,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return true; return true;
} }
/// <summary>
/// 当当前直接注册项属于请求处理器时,提取 request invoker provider 所需的请求/响应类型显示名。
/// </summary>
private static bool TryCreateRequestInvokerRegistrationSpec(
INamedTypeSymbol handlerInterface,
out RequestInvokerRegistrationSpec requestInvokerRegistration)
{
if (!string.Equals(
handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
$"global::{CqrsContractsNamespace}.IRequestHandler<TRequest, TResponse>",
StringComparison.Ordinal))
{
requestInvokerRegistration = default;
return false;
}
if (handlerInterface.TypeArguments.Length != 2)
{
requestInvokerRegistration = default;
return false;
}
requestInvokerRegistration = new RequestInvokerRegistrationSpec(
handlerInterface.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
handlerInterface.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
return true;
}
/// <summary> /// <summary>
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。 /// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
@ -296,7 +340,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
context.AddSource( context.AddSource(
HintName, HintName,
GenerateSource(registrations, reflectionFallbackEmission)); GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission));
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,177 @@
using System.Reflection;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 generated request invoker provider 的 registrar 接线与 dispatcher 消费语义。
/// </summary>
[TestFixture]
[NonParallelizable]
internal sealed class CqrsGeneratedRequestInvokerProviderTests
{
/// <summary>
/// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
ClearRegistrarCaches();
ClearDispatcherCaches();
}
/// <summary>
/// 在每个用例后清理静态缓存。
/// </summary>
[TearDown]
public void TearDown()
{
ClearRegistrarCaches();
ClearDispatcherCaches();
}
/// <summary>
/// 验证 registrar 激活 generated registry 后,会把 request invoker provider 注册到容器中。
/// </summary>
[Test]
public void RegisterHandlers_Should_Register_Generated_Request_Invoker_Provider()
{
var generatedAssembly = CreateGeneratedRequestInvokerAssembly();
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
var providers = container.GetAll<ICqrsRequestInvokerProvider>();
Assert.That(
providers.Select(static provider => provider.GetType()),
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
}
/// <summary>
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
/// </summary>
[Test]
public async Task SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered()
{
var generatedAssembly = CreateGeneratedRequestInvokerAssembly();
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload"));
var requestBindings = GetDispatcherCacheField("RequestDispatchBindings");
var binding = GetRequestDispatchBindingValue(
requestBindings,
typeof(GeneratedRequestInvokerRequest),
typeof(string));
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo("generated:payload"));
Assert.That(binding, Is.Not.Null);
});
}
/// <summary>
/// 创建带有 generated request invoker registry 元数据的程序集替身。
/// </summary>
private static Mock<Assembly> CreateGeneratedRequestInvokerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Cqrs.Tests.Cqrs.GeneratedRequestInvokerAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedRequestInvokerProviderRegistry))]);
return generatedAssembly;
}
/// <summary>
/// 清空 registrar 静态缓存。
/// </summary>
private static void ClearRegistrarCaches()
{
ClearCache(GetRegistrarCacheField("AssemblyMetadataCache"));
ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache"));
ClearCache(GetRegistrarCacheField("LoadableTypesCache"));
ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
}
/// <summary>
/// 清空 dispatcher 静态缓存。
/// </summary>
private static void ClearDispatcherCaches()
{
ClearCache(GetDispatcherCacheField("NotificationDispatchBindings"));
ClearCache(GetDispatcherCacheField("RequestDispatchBindings"));
ClearCache(GetDispatcherCacheField("StreamDispatchBindings"));
ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers"));
}
/// <summary>
/// 通过反射读取 registrar 的静态缓存字段。
/// </summary>
private static object GetRegistrarCacheField(string fieldName)
{
var field = typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!
.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}.");
return field!.GetValue(null)
?? throw new InvalidOperationException($"Registrar cache field {fieldName} returned null.");
}
/// <summary>
/// 通过反射读取 dispatcher 的静态缓存字段。
/// </summary>
private static object GetDispatcherCacheField(string fieldName)
{
var field = typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!
.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}.");
return field!.GetValue(null)
?? throw new InvalidOperationException($"Dispatcher cache field {fieldName} returned null.");
}
/// <summary>
/// 清空目标缓存实例。
/// </summary>
private static void ClearCache(object cache)
{
_ = cache.GetType()
.GetMethod("Clear", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.Invoke(cache, Array.Empty<object>());
}
/// <summary>
/// 读取指定请求/响应类型对当前缓存的 request dispatch binding。
/// </summary>
private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType)
{
var bindingBox = requestBindings.GetType()
.GetMethod("GetValueOrDefaultForTesting", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.Invoke(requestBindings, [requestType, responseType]);
if (bindingBox is null)
{
return null;
}
return bindingBox.GetType()
.GetMethod("Get", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.MakeGenericMethod(responseType)
.Invoke(bindingBox, Array.Empty<object>());
}
}

View File

@ -0,0 +1,90 @@
using System.Reflection;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 模拟同时提供 handler 注册与 request invoker 元数据的 generated registry。
/// </summary>
internal sealed class GeneratedRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptor Descriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
Descriptor);
/// <summary>
/// 将测试请求处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRequestInvokerRequestHandler).FullName} as {typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>).FullName}.");
}
/// <summary>
/// 尝试返回指定 request/response 类型对对应的 generated invoker 描述符。
/// </summary>
/// <param name="requestType">请求运行时类型。</param>
/// <param name="responseType">响应运行时类型。</param>
/// <param name="descriptor">命中时返回的描述符。</param>
/// <returns>若类型对匹配当前测试请求则返回 <see langword="true" />。</returns>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 返回当前 registry 暴露的全部 generated request invoker 描述符。
/// </summary>
/// <returns>单条测试 request invoker 描述符条目。</returns>
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return [DescriptorEntry];
}
/// <summary>
/// 模拟 generated request invoker 直接执行后的返回值。
/// </summary>
/// <param name="handler">当前请求处理器实例。</param>
/// <param name="request">当前测试请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>带有 generated 前缀的结果,便于断言 dispatcher 走了 provider 路径。</returns>
private static ValueTask<string> InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
{
_ = handler as IRequestHandler<GeneratedRequestInvokerRequest, string>
?? throw new InvalidOperationException("Generated invoker received an incompatible handler instance.");
var typedRequest = (GeneratedRequestInvokerRequest)request;
return ValueTask.FromResult($"generated:{typedRequest.Value}");
}
}

View File

@ -0,0 +1,8 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 用于验证 generated request invoker provider 接线的测试请求。
/// </summary>
internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest<string>;

View File

@ -0,0 +1,21 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 供 generated request invoker provider 测试使用的请求处理器。
/// </summary>
internal sealed class GeneratedRequestInvokerRequestHandler : IRequestHandler<GeneratedRequestInvokerRequest, string>
{
/// <summary>
/// 返回带有运行时处理器前缀的结果,便于和 generated invoker 自定义结果区分。
/// </summary>
/// <param name="request">当前测试请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>运行时处理器生成的响应字符串。</returns>
public ValueTask<string> Handle(GeneratedRequestInvokerRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
return ValueTask.FromResult($"runtime:{request.Value}");
}
}

View File

@ -0,0 +1,31 @@
using System.Reflection;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs;
/// <summary>
/// 描述单个 request/response 类型对在运行时分发时需要复用的元数据。
/// </summary>
/// <param name="handlerType">当前请求处理器在容器中的服务类型。</param>
/// <param name="invokerMethod">
/// 执行单个请求处理器的开放静态方法。
/// dispatcher 会在首次创建 request binding 时,把该方法绑定成内部使用的强类型委托。
/// </param>
/// <remarks>
/// dispatcher 会继续自行构造 pipeline behavior 服务类型并负责上下文注入;
/// 该描述符只前移请求处理器服务类型与直接调用方法元数据。
/// </remarks>
public sealed class CqrsRequestInvokerDescriptor(
Type handlerType,
MethodInfo invokerMethod)
{
/// <summary>
/// 获取请求处理器在容器中的服务类型。
/// </summary>
public Type HandlerType { get; } = handlerType ?? throw new ArgumentNullException(nameof(handlerType));
/// <summary>
/// 获取执行请求处理器的开放静态方法。
/// </summary>
public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod));
}

View File

@ -0,0 +1,12 @@
namespace GFramework.Cqrs;
/// <summary>
/// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。
/// </summary>
/// <param name="RequestType">请求运行时类型。</param>
/// <param name="ResponseType">响应运行时类型。</param>
/// <param name="Descriptor">对应的 generated request invoker 描述符。</param>
public sealed record CqrsRequestInvokerDescriptorEntry(
Type RequestType,
Type ResponseType,
CqrsRequestInvokerDescriptor Descriptor);

View File

@ -0,0 +1,26 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs;
/// <summary>
/// 定义由源码生成器或手写注册器提供的 request invoker 元数据契约。
/// </summary>
/// <remarks>
/// 该 seam 允许运行时在首次创建 request dispatch binding 时,
/// 直接复用编译期已知的请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。
/// 当当前程序集没有提供匹配项时dispatcher 仍会回退到既有的反射绑定创建路径。
/// </remarks>
public interface ICqrsRequestInvokerProvider
{
/// <summary>
/// 尝试为指定请求/响应类型对提供运行时元数据。
/// </summary>
/// <param name="requestType">请求运行时类型。</param>
/// <param name="responseType">响应运行时类型。</param>
/// <param name="descriptor">命中时返回的 request invoker 元数据。</param>
/// <returns>若当前 provider 可处理该请求/响应类型对则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor);
}

View File

@ -0,0 +1,18 @@
namespace GFramework.Cqrs;
/// <summary>
/// 为 generated request invoker provider 暴露可枚举描述符集合的内部辅助契约。
/// </summary>
/// <remarks>
/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 request invoker 描述符,
/// 并把它们登记到 dispatcher 的进程级弱缓存中。
/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。
/// </remarks>
public interface IEnumeratesCqrsRequestInvokerDescriptors
{
/// <summary>
/// 返回当前 provider 可声明的全部 request invoker 描述符条目。
/// </summary>
/// <returns>按 provider 定义顺序枚举的描述符条目集合。</returns>
IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors();
}

View File

@ -18,6 +18,11 @@ internal sealed class CqrsDispatcher(
ILogger logger, ILogger logger,
INotificationPublisher notificationPublisher) : ICqrsRuntime INotificationPublisher notificationPublisher) : ICqrsRuntime
{ {
// 卸载安全的进程级缓存:当 generated registry 提供 request invoker 元数据时,
// registrar 会按请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
GeneratedRequestInvokers = new();
// 卸载安全的进程级缓存:通知类型只以弱键语义保留。 // 卸载安全的进程级缓存:通知类型只以弱键语义保留。
// 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。 // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
private static readonly WeakKeyCache<Type, NotificationDispatchBinding> private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
@ -169,6 +174,17 @@ internal sealed class CqrsDispatcher(
/// </summary> /// </summary>
private static RequestDispatchBinding<TResponse> CreateRequestDispatchBinding<TResponse>(Type requestType) private static RequestDispatchBinding<TResponse> CreateRequestDispatchBinding<TResponse>(Type requestType)
{ {
var generatedDescriptor = TryGetGeneratedRequestInvokerDescriptor<TResponse>(requestType);
if (generatedDescriptor is not null)
{
var resolvedGeneratedDescriptor = generatedDescriptor.Value;
return new RequestDispatchBinding<TResponse>(
resolvedGeneratedDescriptor.HandlerType,
typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
resolvedGeneratedDescriptor.Invoker,
requestType);
}
return new RequestDispatchBinding<TResponse>( return new RequestDispatchBinding<TResponse>(
typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)), typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)),
typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
@ -203,6 +219,48 @@ internal sealed class CqrsDispatcher(
return RequestDispatchBindingBox.Create(CreateRequestDispatchBinding<TResponse>(requestType)); return RequestDispatchBindingBox.Create(CreateRequestDispatchBinding<TResponse>(requestType));
} }
/// <summary>
/// 尝试从容器已注册的 generated request invoker provider 中获取指定请求/响应类型对的元数据。
/// </summary>
/// <typeparam name="TResponse">当前请求响应类型。</typeparam>
/// <param name="requestType">请求运行时类型。</param>
/// <returns>命中时返回强类型化后的描述符;否则返回 <see langword="null" />。</returns>
private static RequestInvokerDescriptor<TResponse>? TryGetGeneratedRequestInvokerDescriptor<TResponse>(Type requestType)
{
return GeneratedRequestInvokers.TryGetValue(requestType, typeof(TResponse), out var metadata) &&
metadata is not null
? CreateRequestInvokerDescriptor<TResponse>(requestType, metadata)
: null;
}
/// <summary>
/// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的强类型 request invoker 描述符。
/// </summary>
/// <typeparam name="TResponse">当前请求响应类型。</typeparam>
/// <param name="requestType">请求运行时类型。</param>
/// <param name="descriptor">provider 返回的弱类型描述符。</param>
/// <returns>可直接用于创建 request dispatch binding 的强类型描述符。</returns>
/// <exception cref="InvalidOperationException">当 provider 返回的委托签名与当前请求/响应类型对不匹配时抛出。</exception>
private static RequestInvokerDescriptor<TResponse> CreateRequestInvokerDescriptor<TResponse>(
Type requestType,
GeneratedRequestInvokerMetadata descriptor)
{
if (!descriptor.InvokerMethod.IsStatic)
{
throw new InvalidOperationException(
$"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
}
if (Delegate.CreateDelegate(typeof(RequestInvoker<TResponse>), descriptor.InvokerMethod) is not
RequestInvoker<TResponse> invoker)
{
throw new InvalidOperationException(
$"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
}
return new RequestInvokerDescriptor<TResponse>(descriptor.HandlerType, invoker);
}
/// <summary> /// <summary>
/// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 /// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。
/// </summary> /// </summary>
@ -579,6 +637,46 @@ internal sealed class CqrsDispatcher(
private readonly record struct RequestPipelineExecutorFactoryState<TResponse>( private readonly record struct RequestPipelineExecutorFactoryState<TResponse>(
RequestPipelineInvoker<TResponse> PipelineInvoker); RequestPipelineInvoker<TResponse> PipelineInvoker);
/// <summary>
/// 记录 registrar 写入的 generated request invoker 元数据。
/// </summary>
/// <param name="HandlerType">请求处理器在容器中的服务类型。</param>
/// <param name="InvokerMethod">执行请求处理器的开放静态方法。</param>
private sealed record GeneratedRequestInvokerMetadata(
Type HandlerType,
MethodInfo InvokerMethod);
/// <summary>
/// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。
/// </summary>
/// <typeparam name="TResponse">当前请求响应类型。</typeparam>
private readonly record struct RequestInvokerDescriptor<TResponse>(
Type HandlerType,
RequestInvoker<TResponse> Invoker);
/// <summary>
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
/// </summary>
/// <param name="requestType">请求运行时类型。</param>
/// <param name="responseType">响应运行时类型。</param>
/// <param name="descriptor">要登记的 generated request invoker 描述符。</param>
internal static void RegisterGeneratedRequestInvokerDescriptor(
Type requestType,
Type responseType,
CqrsRequestInvokerDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
ArgumentNullException.ThrowIfNull(descriptor);
_ = GeneratedRequestInvokers.GetOrAdd(
requestType,
responseType,
(_, _) => new GeneratedRequestInvokerMetadata(
descriptor.HandlerType,
descriptor.InvokerMethod));
}
/// <summary> /// <summary>
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。

View File

@ -239,6 +239,62 @@ internal static class CqrsHandlerRegistrar
logger.Debug( logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger); registry.Register(services, logger);
RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger);
}
}
/// <summary>
/// 当 generated registry 同时提供 request invoker 元数据时,把该 provider 注册到当前容器中。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="registry">当前已激活的 generated registry。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <remarks>
/// provider 作为 registry 的附加能力注册到容器后dispatcher 才能在首次请求分发时优先消费编译期生成的 invoker 元数据。
/// 若 registry 不实现该契约,则保持现有纯反射 request binding 创建语义。
/// </remarks>
private static void RegisterGeneratedRequestInvokerProvider(
IServiceCollection services,
ICqrsHandlerRegistry registry,
string assemblyName,
ILogger logger)
{
if (registry is not ICqrsRequestInvokerProvider provider)
return;
services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider);
RegisterGeneratedRequestInvokerDescriptors(provider, assemblyName, logger);
logger.Debug(
$"Registered CQRS request invoker provider {provider.GetType().FullName} for assembly {assemblyName}.");
}
/// <summary>
/// 读取 generated request invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。
/// </summary>
/// <param name="provider">当前已激活的 request invoker provider。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <remarks>
/// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。
/// 这样 request dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。
/// </remarks>
private static void RegisterGeneratedRequestInvokerDescriptors(
ICqrsRequestInvokerProvider provider,
string assemblyName,
ILogger logger)
{
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
return;
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
{
CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
descriptorEntry.RequestType,
descriptorEntry.ResponseType,
descriptorEntry.Descriptor);
logger.Debug(
$"Registered generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from assembly {assemblyName}.");
} }
} }

View File

@ -1689,6 +1689,107 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
private const string RequestInvokerProviderSource = """
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
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>
{
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
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);
}
public interface ICqrsRequestInvokerProvider
{
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor);
}
public interface IEnumeratesCqrsRequestInvokerDescriptors
{
IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors();
}
public sealed class CqrsRequestInvokerDescriptor
{
public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
}
public sealed class CqrsRequestInvokerDescriptorEntry
{
public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor)
{
RequestType = requestType;
ResponseType = responseType;
Descriptor = descriptor;
}
public Type RequestType { get; }
public Type ResponseType { get; }
public CqrsRequestInvokerDescriptor Descriptor { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleRequest(string Value) : IRequest<string>;
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string>
{
public ValueTask<string> Handle(VisibleRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(request.Value);
}
}
}
""";
/// <summary> /// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary> /// </summary>
@ -2240,6 +2341,55 @@ public class CqrsHandlerRegistryGeneratorTests
}); });
} }
/// <summary>
/// 验证当 runtime 暴露 request invoker provider 契约时,生成器会让 generated registry 同时发射
/// request invoker 描述符与对应的开放静态 invoker 方法。
/// </summary>
[Test]
public void Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available()
{
var execution = ExecuteGenerator(RequestInvokerProviderSource);
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();
var generatedSource = execution.GeneratedSources[0].content;
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors, Is.Empty);
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(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsRequestInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.VisibleRequest), typeof(string),"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<global::TestApp.VisibleRequest, string>), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)"));
Assert.That(
generatedSource,
Does.Contain(
"private static global::System.Threading.Tasks.ValueTask<string> InvokeRequestHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)"));
Assert.That(
generatedSource,
Does.Contain(
"public global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()"));
});
}
/// <summary> /// <summary>
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
/// </summary> /// </summary>

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-066` - 恢复点编号:`CQRS-REWRITE-RP-067`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前焦点: - 当前焦点:
- 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
@ -40,6 +40,15 @@ CQRS 迁移与收敛。
- `GFramework.Core.Abstractions/README.md``docs/zh-CN/abstractions/core-abstractions.md` - `GFramework.Core.Abstractions/README.md``docs/zh-CN/abstractions/core-abstractions.md`
`docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留, `docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留,
新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` 新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`
- 已完成一轮 `dispatch/invoker` 生成前移的最小 request 切片:
- `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider``IEnumeratesCqrsRequestInvokerDescriptors`
`CqrsRequestInvokerDescriptor``CqrsRequestInvokerDescriptorEntry`
- generated registry 若实现 request invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器,
并把 provider 枚举出的 request invoker 描述符写入 dispatcher 的进程级弱缓存
- `CqrsDispatcher` 现会在首次创建 request dispatch binding 时优先命中 generated request invoker 描述符;
未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径
- `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义
- `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时generated registry 会额外发射 request invoker provider 成员与 invoker 方法
- 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
@ -228,6 +237,15 @@ CQRS 迁移与收敛。
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过 - 结果:通过
- 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警 - 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
- 结果:通过
- 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过 - 结果:通过
- 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净 - 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净
@ -238,5 +256,5 @@ CQRS 迁移与收敛。
## 下一步 ## 下一步
1. 基于已落地的 notification publisher seam评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator 1. 基于已落地的 notification publisher seam评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator
2. 继续以 `dispatch/invoker` 生成前移为优先对象,优先尝试 “generated request invoker provider + dispatcher fallback” 这条最小实现切片 2. 基于已落地的 request invoker provider评估是否继续把 notification / stream 的 invoker 也前移,或先补 provider 发现/诊断与文档入口
3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级 3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级

View File

@ -2,6 +2,39 @@
## 2026-04-30 ## 2026-04-30
### 阶段generated request invoker provider 最小落地CQRS-REWRITE-RP-067
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
- 在 `RP-066` 提交后复算 branch diff相对 `origin/main` 增长到 `22 files`,仍明显低于 `50 files` stop condition因此继续下一批
- 本轮 critical path 保持在主线程,本地完成 `dispatch/invoker` 生成前移的最小 request 切片;尝试委派 source-generator 测试给 worker 时因 subagent 名额已满失败,因此主线程直接接管该测试修改
- 本轮关键设计调整:
- 不按 `requestType.Assembly` 做 provider 发现,避免“请求定义在 A、handler 与 generated registry 在 B”时漏掉 generated invoker
- generated registry 若实现 `ICqrsRequestInvokerProvider`registrar 会在激活 registry 后把 provider 注册进容器,并通过 `IEnumeratesCqrsRequestInvokerDescriptors` 把描述符写入 dispatcher 的进程级弱缓存
- dispatcher 首次创建 request dispatch binding 时只按 `requestType + responseType` 读取静态弱缓存,不依赖具体容器实例;未命中时仍走既有反射创建路径
- 已完成实现:
- `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider``IEnumeratesCqrsRequestInvokerDescriptors`
`CqrsRequestInvokerDescriptor``CqrsRequestInvokerDescriptorEntry`
- `CqrsHandlerRegistrar` 现会识别 generated registry 的 request invoker provider 能力,并登记 provider 与 request invoker 描述符
- `CqrsDispatcher` 新增 generated request invoker 弱缓存,并在 request binding 创建时优先消费该元数据
- `CqrsHandlerRegistryGenerator` 在 runtime 合同可用时,会让 generated registry 额外实现 request invoker provider 相关接口,并发射 descriptor 列表、`TryGetDescriptor(...)``GetDescriptors()` 与 request invoker 静态方法
- 已补充测试:
- `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果
- `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法
### 验证
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
- 结果:通过,`22/22` passed
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过,`1/1` passed
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
### 当前下一步
1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义
2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files`
### 阶段LegacyICqrsRuntime compatibility slice 收口CQRS-REWRITE-RP-066 ### 阶段LegacyICqrsRuntime compatibility slice 收口CQRS-REWRITE-RP-066
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` - 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`