From 0c65cd8e38412530b4d0f141a6e2327bdb606558 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:10:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(cqrs):=20=E5=89=8D=E7=A7=BB=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=B0=83=E7=94=A8=E5=99=A8=E7=94=9F=E6=88=90=E6=B3=A8?= =?UTF-8?q?=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 generated request invoker provider seam,并让 registrar 与 dispatcher 复用编译期请求调用元数据 - 扩展 CQRS source generator 发射 request invoker provider 成员与最小 request invoker 方法 - 补充 runtime 与 source-generator 回归测试,并更新 cqrs-rewrite 追踪到 RP-067 --- .../CqrsHandlerRegistryGenerator.Models.cs | 22 ++- ...HandlerRegistryGenerator.SourceEmission.cs | 153 ++++++++++++++- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 50 ++++- ...qrsGeneratedRequestInvokerProviderTests.cs | 177 ++++++++++++++++++ ...GeneratedRequestInvokerProviderRegistry.cs | 90 +++++++++ .../Cqrs/GeneratedRequestInvokerRequest.cs | 8 + .../GeneratedRequestInvokerRequestHandler.cs | 21 +++ .../CqrsRequestInvokerDescriptor.cs | 31 +++ .../CqrsRequestInvokerDescriptorEntry.cs | 12 ++ .../ICqrsRequestInvokerProvider.cs | 26 +++ ...EnumeratesCqrsRequestInvokerDescriptors.cs | 18 ++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 98 ++++++++++ .../Internal/CqrsHandlerRegistrar.cs | 56 ++++++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 150 +++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 22 ++- .../traces/cqrs-rewrite-migration-trace.md | 33 ++++ 16 files changed, 956 insertions(+), 11 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs create mode 100644 GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs create mode 100644 GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs create mode 100644 GFramework.Cqrs/ICqrsRequestInvokerProvider.cs create mode 100644 GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index ccd7e8cb..beb22f1e 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -5,11 +5,16 @@ namespace GFramework.Cqrs.SourceGenerators.Cqrs; /// public sealed partial class CqrsHandlerRegistryGenerator { + private readonly record struct RequestInvokerRegistrationSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName); + private readonly record struct HandlerRegistrationSpec( string HandlerInterfaceDisplayName, string ImplementationTypeDisplayName, string HandlerInterfaceLogName, - string ImplementationLogName); + string ImplementationLogName, + RequestInvokerRegistrationSpec? RequestInvokerRegistration); private readonly record struct ReflectedImplementationRegistrationSpec( string HandlerInterfaceDisplayName, @@ -24,14 +29,24 @@ public sealed partial class CqrsHandlerRegistryGenerator bool HasReflectedImplementationRegistrations, bool HasPreciseReflectedRegistrations, bool HasReflectionTypeLookups, - bool HasExternalAssemblyTypeLookups) + bool HasExternalAssemblyTypeLookups, + bool SupportsRequestInvokerProvider, + ImmutableArray RequestInvokerEmissions) { public bool RequiresRegistryAssemblyVariable => HasReflectedImplementationRegistrations || HasPreciseReflectedRegistrations || HasReflectionTypeLookups; + + public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty; } + private readonly record struct RequestInvokerEmissionSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName, + string HandlerInterfaceDisplayName, + int MethodIndex); + /// /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 /// @@ -312,5 +327,6 @@ public sealed partial class CqrsHandlerRegistryGenerator bool GenerationEnabled, bool SupportsNamedReflectionFallbackTypes, bool SupportsDirectReflectionFallbackTypes, - bool SupportsMultipleReflectionFallbackAttributes); + bool SupportsMultipleReflectionFallbackAttributes, + bool SupportsRequestInvokerProvider); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 9dd72b7f..a408d508 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -25,10 +25,11 @@ public sealed partial class CqrsHandlerRegistryGenerator /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 /// private static string GenerateSource( + GenerationEnvironment generationEnvironment, IReadOnlyList registrations, ReflectionFallbackEmissionSpec reflectionFallbackEmission) { - var sourceShape = CreateGeneratedRegistrySourceShape(registrations); + var sourceShape = CreateGeneratedRegistrySourceShape(generationEnvironment, registrations); var builder = new StringBuilder(); AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission); AppendGeneratedRegistryType(builder, registrations, sourceShape); @@ -41,6 +42,7 @@ public sealed partial class CqrsHandlerRegistryGenerator /// 已整理并排序的 handler 注册描述。 /// 当前生成输出需要启用的结构分支。 private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape( + GenerationEnvironment generationEnvironment, IReadOnlyList registrations) { var hasReflectedImplementationRegistrations = registrations.Any(static registration => @@ -52,12 +54,44 @@ public sealed partial class CqrsHandlerRegistryGenerator var hasExternalAssemblyTypeLookups = registrations.Any(static registration => registration.PreciseReflectedRegistrations.Any(static preciseRegistration => preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); + var requestInvokerEmissions = CreateRequestInvokerEmissions( + generationEnvironment.SupportsRequestInvokerProvider, + registrations); return new GeneratedRegistrySourceShape( hasReflectedImplementationRegistrations, hasPreciseReflectedRegistrations, hasReflectionTypeLookups, - hasExternalAssemblyTypeLookups); + hasExternalAssemblyTypeLookups, + generationEnvironment.SupportsRequestInvokerProvider, + requestInvokerEmissions); + } + + private static ImmutableArray CreateRequestInvokerEmissions( + bool supportsRequestInvokerProvider, + IReadOnlyList registrations) + { + if (!supportsRequestInvokerProvider) + return ImmutableArray.Empty; + + var builder = ImmutableArray.CreateBuilder(); + 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(); } /// @@ -160,10 +194,26 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.Append(GeneratedTypeName); builder.Append(" : global::"); 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("{"); AppendRegisterMethod(builder, registrations, sourceShape); + if (sourceShape.HasRequestInvokerProvider) + { + builder.AppendLine(); + AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions); + } + if (sourceShape.HasExternalAssemblyTypeLookups) { builder.AppendLine(); @@ -223,6 +273,103 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(" }"); } + private static void AppendRequestInvokerProviderMembers( + StringBuilder builder, + ImmutableArray 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 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 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( StringBuilder builder, ImplementationRegistrationSpec registration) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 5ebe31b0..db5f1bc7 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -15,6 +15,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1"; private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; 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 = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; @@ -66,6 +73,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator CqrsHandlerRegistryAttributeMetadataName) is not null && compilation.GetTypeByMetadataName(ILoggerMetadataName) 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 typeType = compilation.GetTypeByMetadataName("System.Type"); var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null && @@ -85,7 +97,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator generationEnabled, supportsNamedReflectionFallbackTypes, supportsDirectReflectionFallbackTypes, - supportsMultipleReflectionFallbackAttributes); + supportsMultipleReflectionFallbackAttributes, + supportsRequestInvokerProvider); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -218,7 +231,10 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), implementationTypeDisplayName, GetLogDisplayName(handlerInterface), - implementationLogName)); + implementationLogName, + TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration) + ? requestInvokerRegistration + : null)); return true; } @@ -237,6 +253,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator return true; } + /// + /// 当当前直接注册项属于请求处理器时,提取 request invoker provider 所需的请求/响应类型显示名。 + /// + private static bool TryCreateRequestInvokerRegistrationSpec( + INamedTypeSymbol handlerInterface, + out RequestInvokerRegistrationSpec requestInvokerRegistration) + { + if (!string.Equals( + handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + $"global::{CqrsContractsNamespace}.IRequestHandler", + 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; + } + /// /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 /// CqrsHandlerRegistry.g.cs,并在需要时附带程序集级 reflection fallback 元数据。 @@ -296,7 +340,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator context.AddSource( HintName, - GenerateSource(registrations, reflectionFallbackEmission)); + GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission)); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs new file mode 100644 index 00000000..0919bc79 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -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; + +/// +/// 验证 generated request invoker provider 的 registrar 接线与 dispatcher 消费语义。 +/// +[TestFixture] +[NonParallelizable] +internal sealed class CqrsGeneratedRequestInvokerProviderTests +{ + /// + /// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。 + /// + [SetUp] + public void SetUp() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + ClearRegistrarCaches(); + ClearDispatcherCaches(); + } + + /// + /// 在每个用例后清理静态缓存。 + /// + [TearDown] + public void TearDown() + { + ClearRegistrarCaches(); + ClearDispatcherCaches(); + } + + /// + /// 验证 registrar 激活 generated registry 后,会把 request invoker provider 注册到容器中。 + /// + [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(); + + Assert.That( + providers.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); + } + + /// + /// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。 + /// + [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); + }); + } + + /// + /// 创建带有 generated request invoker registry 元数据的程序集替身。 + /// + private static Mock CreateGeneratedRequestInvokerAssembly() + { + var generatedAssembly = new Mock(); + 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; + } + + /// + /// 清空 registrar 静态缓存。 + /// + private static void ClearRegistrarCaches() + { + ClearCache(GetRegistrarCacheField("AssemblyMetadataCache")); + ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache")); + ClearCache(GetRegistrarCacheField("LoadableTypesCache")); + ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache")); + } + + /// + /// 清空 dispatcher 静态缓存。 + /// + private static void ClearDispatcherCaches() + { + ClearCache(GetDispatcherCacheField("NotificationDispatchBindings")); + ClearCache(GetDispatcherCacheField("RequestDispatchBindings")); + ClearCache(GetDispatcherCacheField("StreamDispatchBindings")); + ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers")); + } + + /// + /// 通过反射读取 registrar 的静态缓存字段。 + /// + 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."); + } + + /// + /// 通过反射读取 dispatcher 的静态缓存字段。 + /// + 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."); + } + + /// + /// 清空目标缓存实例。 + /// + private static void ClearCache(object cache) + { + _ = cache.GetType() + .GetMethod("Clear", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)! + .Invoke(cache, Array.Empty()); + } + + /// + /// 读取指定请求/响应类型对当前缓存的 request dispatch binding。 + /// + 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()); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs new file mode 100644 index 00000000..bad287b6 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs @@ -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; + +/// +/// 模拟同时提供 handler 注册与 request invoker 元数据的 generated registry。 +/// +internal sealed class GeneratedRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors +{ + private static readonly CqrsRequestInvokerDescriptor Descriptor = new( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + Descriptor); + + /// + /// 将测试请求处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRequestInvokerRequestHandler).FullName} as {typeof(IRequestHandler).FullName}."); + } + + /// + /// 尝试返回指定 request/response 类型对对应的 generated invoker 描述符。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 命中时返回的描述符。 + /// 若类型对匹配当前测试请求则返回 + 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; + } + + /// + /// 返回当前 registry 暴露的全部 generated request invoker 描述符。 + /// + /// 单条测试 request invoker 描述符条目。 + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + /// + /// 模拟 generated request invoker 直接执行后的返回值。 + /// + /// 当前请求处理器实例。 + /// 当前测试请求。 + /// 取消令牌。 + /// 带有 generated 前缀的结果,便于断言 dispatcher 走了 provider 路径。 + private static ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + _ = handler as IRequestHandler + ?? throw new InvalidOperationException("Generated invoker received an incompatible handler instance."); + var typedRequest = (GeneratedRequestInvokerRequest)request; + return ValueTask.FromResult($"generated:{typedRequest.Value}"); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs new file mode 100644 index 00000000..e26ef2c2 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs @@ -0,0 +1,8 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 用于验证 generated request invoker provider 接线的测试请求。 +/// +internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs new file mode 100644 index 00000000..2127b216 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs @@ -0,0 +1,21 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 供 generated request invoker provider 测试使用的请求处理器。 +/// +internal sealed class GeneratedRequestInvokerRequestHandler : IRequestHandler +{ + /// + /// 返回带有运行时处理器前缀的结果,便于和 generated invoker 自定义结果区分。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 运行时处理器生成的响应字符串。 + public ValueTask Handle(GeneratedRequestInvokerRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + return ValueTask.FromResult($"runtime:{request.Value}"); + } +} diff --git a/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs b/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs new file mode 100644 index 00000000..cc7cce14 --- /dev/null +++ b/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 描述单个 request/response 类型对在运行时分发时需要复用的元数据。 +/// +/// 当前请求处理器在容器中的服务类型。 +/// +/// 执行单个请求处理器的开放静态方法。 +/// dispatcher 会在首次创建 request binding 时,把该方法绑定成内部使用的强类型委托。 +/// +/// +/// dispatcher 会继续自行构造 pipeline behavior 服务类型并负责上下文注入; +/// 该描述符只前移请求处理器服务类型与直接调用方法元数据。 +/// +public sealed class CqrsRequestInvokerDescriptor( + Type handlerType, + MethodInfo invokerMethod) +{ + /// + /// 获取请求处理器在容器中的服务类型。 + /// + public Type HandlerType { get; } = handlerType ?? throw new ArgumentNullException(nameof(handlerType)); + + /// + /// 获取执行请求处理器的开放静态方法。 + /// + public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod)); +} diff --git a/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs b/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs new file mode 100644 index 00000000..679652b6 --- /dev/null +++ b/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs @@ -0,0 +1,12 @@ +namespace GFramework.Cqrs; + +/// +/// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。 +/// +/// 请求运行时类型。 +/// 响应运行时类型。 +/// 对应的 generated request invoker 描述符。 +public sealed record CqrsRequestInvokerDescriptorEntry( + Type RequestType, + Type ResponseType, + CqrsRequestInvokerDescriptor Descriptor); diff --git a/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs b/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs new file mode 100644 index 00000000..da3c69a1 --- /dev/null +++ b/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs @@ -0,0 +1,26 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 定义由源码生成器或手写注册器提供的 request invoker 元数据契约。 +/// +/// +/// 该 seam 允许运行时在首次创建 request dispatch binding 时, +/// 直接复用编译期已知的请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。 +/// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射绑定创建路径。 +/// +public interface ICqrsRequestInvokerProvider +{ + /// + /// 尝试为指定请求/响应类型对提供运行时元数据。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 命中时返回的 request invoker 元数据。 + /// 若当前 provider 可处理该请求/响应类型对则返回 ;否则返回 + bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor); +} diff --git a/GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs b/GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs new file mode 100644 index 00000000..0aa08698 --- /dev/null +++ b/GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs; + +/// +/// 为 generated request invoker provider 暴露可枚举描述符集合的内部辅助契约。 +/// +/// +/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 request invoker 描述符, +/// 并把它们登记到 dispatcher 的进程级弱缓存中。 +/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。 +/// +public interface IEnumeratesCqrsRequestInvokerDescriptors +{ + /// + /// 返回当前 provider 可声明的全部 request invoker 描述符条目。 + /// + /// 按 provider 定义顺序枚举的描述符条目集合。 + IReadOnlyList GetDescriptors(); +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 334186db..a88be732 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -18,6 +18,11 @@ internal sealed class CqrsDispatcher( ILogger logger, INotificationPublisher notificationPublisher) : ICqrsRuntime { + // 卸载安全的进程级缓存:当 generated registry 提供 request invoker 元数据时, + // registrar 会按请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。 + private static readonly WeakTypePairCache + GeneratedRequestInvokers = new(); + // 卸载安全的进程级缓存:通知类型只以弱键语义保留。 // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。 private static readonly WeakKeyCache @@ -169,6 +174,17 @@ internal sealed class CqrsDispatcher( /// private static RequestDispatchBinding CreateRequestDispatchBinding(Type requestType) { + var generatedDescriptor = TryGetGeneratedRequestInvokerDescriptor(requestType); + if (generatedDescriptor is not null) + { + var resolvedGeneratedDescriptor = generatedDescriptor.Value; + return new RequestDispatchBinding( + resolvedGeneratedDescriptor.HandlerType, + typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), + resolvedGeneratedDescriptor.Invoker, + requestType); + } + return new RequestDispatchBinding( typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)), typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), @@ -203,6 +219,48 @@ internal sealed class CqrsDispatcher( return RequestDispatchBindingBox.Create(CreateRequestDispatchBinding(requestType)); } + /// + /// 尝试从容器已注册的 generated request invoker provider 中获取指定请求/响应类型对的元数据。 + /// + /// 当前请求响应类型。 + /// 请求运行时类型。 + /// 命中时返回强类型化后的描述符;否则返回 + private static RequestInvokerDescriptor? TryGetGeneratedRequestInvokerDescriptor(Type requestType) + { + return GeneratedRequestInvokers.TryGetValue(requestType, typeof(TResponse), out var metadata) && + metadata is not null + ? CreateRequestInvokerDescriptor(requestType, metadata) + : null; + } + + /// + /// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的强类型 request invoker 描述符。 + /// + /// 当前请求响应类型。 + /// 请求运行时类型。 + /// provider 返回的弱类型描述符。 + /// 可直接用于创建 request dispatch binding 的强类型描述符。 + /// 当 provider 返回的委托签名与当前请求/响应类型对不匹配时抛出。 + private static RequestInvokerDescriptor CreateRequestInvokerDescriptor( + 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), descriptor.InvokerMethod) is not + RequestInvoker 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(descriptor.HandlerType, invoker); + } + /// /// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 /// @@ -579,6 +637,46 @@ internal sealed class CqrsDispatcher( private readonly record struct RequestPipelineExecutorFactoryState( RequestPipelineInvoker PipelineInvoker); + /// + /// 记录 registrar 写入的 generated request invoker 元数据。 + /// + /// 请求处理器在容器中的服务类型。 + /// 执行请求处理器的开放静态方法。 + private sealed record GeneratedRequestInvokerMetadata( + Type HandlerType, + MethodInfo InvokerMethod); + + /// + /// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。 + /// + /// 当前请求响应类型。 + private readonly record struct RequestInvokerDescriptor( + Type HandlerType, + RequestInvoker Invoker); + + /// + /// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 要登记的 generated request invoker 描述符。 + 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)); + } + /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 1be2729c..000e9ed2 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -239,6 +239,62 @@ internal static class CqrsHandlerRegistrar logger.Debug( $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); registry.Register(services, logger); + RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger); + } + } + + /// + /// 当 generated registry 同时提供 request invoker 元数据时,把该 provider 注册到当前容器中。 + /// + /// 目标服务集合。 + /// 当前已激活的 generated registry。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// provider 作为 registry 的附加能力注册到容器后,dispatcher 才能在首次请求分发时优先消费编译期生成的 invoker 元数据。 + /// 若 registry 不实现该契约,则保持现有纯反射 request binding 创建语义。 + /// + 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}."); + } + + /// + /// 读取 generated request invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。 + /// + /// 当前已激活的 request invoker provider。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。 + /// 这样 request dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。 + /// + 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}."); } } diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 2b83cddc..1dc35bad 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -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 { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + 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); + } + + public interface ICqrsRequestInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsRequestInvokerDescriptors + { + IReadOnlyList 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; + + public sealed class VisibleHandler : IRequestHandler + { + public ValueTask Handle(VisibleRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(request.Value); + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -2240,6 +2341,55 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 暴露 request invoker provider 契约时,生成器会让 generated registry 同时发射 + /// request invoker 描述符与对应的开放静态 invoker 方法。 + /// + [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), 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 InvokeRequestHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// 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 9f9a6df3..20945043 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,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-066` +- 恢复点编号:`CQRS-REWRITE-RP-067` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `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` 与 `docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留, 新代码应直接依赖 `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 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 @@ -228,6 +237,15 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`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` - 结果:通过 - 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净 @@ -238,5 +256,5 @@ CQRS 迁移与收敛。 ## 下一步 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 与专门测试,可暂时移出最高优先级 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 acde156f..e3db6837 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,39 @@ ## 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) - 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`