From ea0b9377055d7ce41d278ca2494894d2f82b1f0f Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:26:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(cqrs):=20=E8=A1=A5=E5=85=85=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=BC=8F=20stream=20invoker=20=E6=8E=A5=E7=BC=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 stream invoker provider、descriptor 与 dispatcher/registrar 接线 - 更新 source generator 与回归测试,覆盖 generated stream invoker 发射和消费语义 - 更新 CQRS 文档与 ai-plan 恢复点,补充 stream invoker 的接入与验证记录 --- .../CqrsHandlerRegistryGenerator.Models.cs | 22 ++- ...HandlerRegistryGenerator.SourceEmission.cs | 179 +++++++++++++++++- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 46 ++++- GFramework.Cqrs.SourceGenerators/README.md | 4 + ...qrsGeneratedRequestInvokerProviderTests.cs | 69 +++++++ .../GeneratedStreamInvokerProviderRegistry.cs | 102 ++++++++++ .../Cqrs/GeneratedStreamInvokerRequest.cs | 9 + .../GeneratedStreamInvokerRequestHandler.cs | 34 ++++ .../CqrsStreamInvokerDescriptor.cs | 31 +++ .../CqrsStreamInvokerDescriptorEntry.cs | 12 ++ GFramework.Cqrs/ICqrsStreamInvokerProvider.cs | 33 ++++ ...IEnumeratesCqrsStreamInvokerDescriptors.cs | 18 ++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 97 ++++++++++ .../Internal/CqrsHandlerRegistrar.cs | 56 ++++++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 5 +- .../todos/cqrs-rewrite-migration-tracking.md | 28 ++- .../traces/cqrs-rewrite-migration-trace.md | 56 ++++++ docs/zh-CN/core/cqrs.md | 2 +- .../cqrs-handler-registry-generator.md | 1 + 19 files changed, 792 insertions(+), 12 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs create mode 100644 GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs create mode 100644 GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs create mode 100644 GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs create mode 100644 GFramework.Cqrs/ICqrsStreamInvokerProvider.cs create mode 100644 GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index beb22f1e..7edbfdac 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -9,12 +9,17 @@ public sealed partial class CqrsHandlerRegistryGenerator string RequestTypeDisplayName, string ResponseTypeDisplayName); + private readonly record struct StreamInvokerRegistrationSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName); + private readonly record struct HandlerRegistrationSpec( string HandlerInterfaceDisplayName, string ImplementationTypeDisplayName, string HandlerInterfaceLogName, string ImplementationLogName, - RequestInvokerRegistrationSpec? RequestInvokerRegistration); + RequestInvokerRegistrationSpec? RequestInvokerRegistration, + StreamInvokerRegistrationSpec? StreamInvokerRegistration); private readonly record struct ReflectedImplementationRegistrationSpec( string HandlerInterfaceDisplayName, @@ -31,7 +36,9 @@ public sealed partial class CqrsHandlerRegistryGenerator bool HasReflectionTypeLookups, bool HasExternalAssemblyTypeLookups, bool SupportsRequestInvokerProvider, - ImmutableArray RequestInvokerEmissions) + ImmutableArray RequestInvokerEmissions, + bool SupportsStreamInvokerProvider, + ImmutableArray StreamInvokerEmissions) { public bool RequiresRegistryAssemblyVariable => HasReflectedImplementationRegistrations || @@ -39,6 +46,8 @@ public sealed partial class CqrsHandlerRegistryGenerator HasReflectionTypeLookups; public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty; + + public bool HasStreamInvokerProvider => SupportsStreamInvokerProvider && !StreamInvokerEmissions.IsDefaultOrEmpty; } private readonly record struct RequestInvokerEmissionSpec( @@ -47,6 +56,12 @@ public sealed partial class CqrsHandlerRegistryGenerator string HandlerInterfaceDisplayName, int MethodIndex); + private readonly record struct StreamInvokerEmissionSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName, + string HandlerInterfaceDisplayName, + int MethodIndex); + /// /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 /// @@ -328,5 +343,6 @@ public sealed partial class CqrsHandlerRegistryGenerator bool SupportsNamedReflectionFallbackTypes, bool SupportsDirectReflectionFallbackTypes, bool SupportsMultipleReflectionFallbackAttributes, - bool SupportsRequestInvokerProvider); + bool SupportsRequestInvokerProvider, + bool SupportsStreamInvokerProvider); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 858c00d9..ae539d23 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -57,6 +57,9 @@ public sealed partial class CqrsHandlerRegistryGenerator var requestInvokerEmissions = CreateRequestInvokerEmissions( generationEnvironment.SupportsRequestInvokerProvider, registrations); + var streamInvokerEmissions = CreateStreamInvokerEmissions( + generationEnvironment.SupportsStreamInvokerProvider, + registrations); return new GeneratedRegistrySourceShape( hasReflectedImplementationRegistrations, @@ -64,7 +67,9 @@ public sealed partial class CqrsHandlerRegistryGenerator hasReflectionTypeLookups, hasExternalAssemblyTypeLookups, generationEnvironment.SupportsRequestInvokerProvider, - requestInvokerEmissions); + requestInvokerEmissions, + generationEnvironment.SupportsStreamInvokerProvider, + streamInvokerEmissions); } /// @@ -111,6 +116,46 @@ public sealed partial class CqrsHandlerRegistryGenerator return builder.ToImmutable(); } + /// + /// 从 direct handler 注册描述中提取 stream invoker 发射计划。 + /// + /// + /// 指示当前 runtime 是否同时暴露 ICqrsStreamInvokerProvider 与 + /// IEnumeratesCqrsStreamInvokerDescriptors 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。 + /// + /// 已按稳定顺序整理完成的 handler 注册描述。 + /// + /// 由 directRegistration.StreamInvokerRegistration 派生出的 集合。 + /// methodIndex 与其 direct registration 的遍历顺序单调递增, + /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 + /// + private static ImmutableArray CreateStreamInvokerEmissions( + bool supportsStreamInvokerProvider, + IReadOnlyList registrations) + { + if (!supportsStreamInvokerProvider) + return ImmutableArray.Empty; + + var builder = ImmutableArray.CreateBuilder(); + var methodIndex = 0; + foreach (var registration in registrations) + { + foreach (var directRegistration in registration.DirectRegistrations) + { + if (directRegistration.StreamInvokerRegistration is not { } streamInvokerRegistration) + continue; + + builder.Add(new StreamInvokerEmissionSpec( + streamInvokerRegistration.RequestTypeDisplayName, + streamInvokerRegistration.ResponseTypeDisplayName, + directRegistration.HandlerInterfaceDisplayName, + methodIndex++)); + } + } + + return builder.ToImmutable(); + } + /// /// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。 /// @@ -221,6 +266,15 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors"); } + if (sourceShape.HasStreamInvokerProvider) + { + builder.Append(", global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".ICqrsStreamInvokerProvider, global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".IEnumeratesCqrsStreamInvokerDescriptors"); + } + builder.AppendLine(); builder.AppendLine("{"); AppendRegisterMethod(builder, registrations, sourceShape); @@ -231,6 +285,12 @@ public sealed partial class CqrsHandlerRegistryGenerator AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions); } + if (sourceShape.HasStreamInvokerProvider) + { + builder.AppendLine(); + AppendStreamInvokerProviderMembers(builder, sourceShape.StreamInvokerEmissions); + } + if (sourceShape.HasExternalAssemblyTypeLookups) { builder.AppendLine(); @@ -366,9 +426,11 @@ public sealed partial class CqrsHandlerRegistryGenerator /// private static void AppendRequestInvokerProviderMethods(StringBuilder builder) { - builder.Append(" public global::System.Collections.Generic.IReadOnlyList GetDescriptors()"); + builder.Append(".CqrsRequestInvokerDescriptorEntry> global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()"); builder.AppendLine(" {"); builder.AppendLine(" return RequestInvokerDescriptors;"); builder.AppendLine(" }"); @@ -424,6 +486,117 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(" }"); } + /// + /// 发射 generated registry 的 stream invoker provider 成员。 + /// + /// 生成源码构造器。 + /// 当前要输出的 stream invoker 发射计划。 + private static void AppendStreamInvokerProviderMembers( + StringBuilder builder, + ImmutableArray streamInvokerEmissions) + { + AppendStreamInvokerDescriptorArray(builder, streamInvokerEmissions); + builder.AppendLine(); + AppendStreamInvokerProviderMethods(builder); + + for (var index = 0; index < streamInvokerEmissions.Length; index++) + { + builder.AppendLine(); + AppendStreamInvokerMethod(builder, streamInvokerEmissions[index]); + } + } + + /// + /// 发射 generated registry 的 stream invoker 描述符数组。 + /// + private static void AppendStreamInvokerDescriptorArray( + StringBuilder builder, + ImmutableArray streamInvokerEmissions) + { + builder.AppendLine(" private static readonly global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry[] StreamInvokerDescriptors ="); + builder.AppendLine(" ["); + + for (var index = 0; index < streamInvokerEmissions.Length; index++) + { + var emission = streamInvokerEmissions[index]; + builder.Append(" new global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsStreamInvokerDescriptorEntry(typeof("); + builder.Append(emission.RequestTypeDisplayName); + builder.Append("), typeof("); + builder.Append(emission.ResponseTypeDisplayName); + builder.Append("), new global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsStreamInvokerDescriptor(typeof("); + builder.Append(emission.HandlerInterfaceDisplayName); + builder.Append("), typeof("); + builder.Append(GeneratedTypeName); + builder.Append(").GetMethod(nameof(InvokeStreamHandler"); + builder.Append(emission.MethodIndex); + builder.Append("), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))"); + builder.AppendLine(index == streamInvokerEmissions.Length - 1 ? string.Empty : ","); + } + + builder.AppendLine(" ];"); + } + + /// + /// 发射 generated registry 对 stream invoker provider 契约的实现方法。 + /// + private static void AppendStreamInvokerProviderMethods(StringBuilder builder) + { + builder.Append(" global::System.Collections.Generic.IReadOnlyList global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".IEnumeratesCqrsStreamInvokerDescriptors.GetDescriptors()"); + builder.AppendLine(" {"); + builder.AppendLine(" return StreamInvokerDescriptors;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.Append(" public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".CqrsStreamInvokerDescriptor? 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 StreamInvokerDescriptors)"); + 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(" }"); + } + + /// + /// 为单个 stream invoker 描述符发射对应的静态强类型桥接方法。 + /// + private static void AppendStreamInvokerMethod(StringBuilder builder, StreamInvokerEmissionSpec emission) + { + builder.Append(" private static object InvokeStreamHandler"); + 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 db5f1bc7..80a2c5a6 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -22,6 +22,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator $"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor"; private const string CqrsRequestInvokerDescriptorEntryMetadataName = $"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry"; + private const string ICqrsStreamInvokerProviderMetadataName = $"{CqrsRuntimeNamespace}.ICqrsStreamInvokerProvider"; + private const string IEnumeratesCqrsStreamInvokerDescriptorsMetadataName = + $"{CqrsRuntimeNamespace}.IEnumeratesCqrsStreamInvokerDescriptors"; + private const string CqrsStreamInvokerDescriptorMetadataName = + $"{CqrsRuntimeNamespace}.CqrsStreamInvokerDescriptor"; + private const string CqrsStreamInvokerDescriptorEntryMetadataName = + $"{CqrsRuntimeNamespace}.CqrsStreamInvokerDescriptorEntry"; private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; @@ -78,6 +85,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null && compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null && compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) is not null; + var supportsStreamInvokerProvider = + compilation.GetTypeByMetadataName(ICqrsStreamInvokerProviderMetadataName) is not null && + compilation.GetTypeByMetadataName(IEnumeratesCqrsStreamInvokerDescriptorsMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsStreamInvokerDescriptorMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsStreamInvokerDescriptorEntryMetadataName) is not null; var stringType = compilation.GetSpecialType(SpecialType.System_String); var typeType = compilation.GetTypeByMetadataName("System.Type"); var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null && @@ -98,7 +110,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator supportsNamedReflectionFallbackTypes, supportsDirectReflectionFallbackTypes, supportsMultipleReflectionFallbackAttributes, - supportsRequestInvokerProvider); + supportsRequestInvokerProvider, + supportsStreamInvokerProvider); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -234,6 +247,9 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator implementationLogName, TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration) ? requestInvokerRegistration + : null, + TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration) + ? streamInvokerRegistration : null)); return true; } @@ -281,6 +297,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator return true; } + /// + /// 当当前直接注册项属于流式请求处理器时,提取 stream invoker provider 所需的请求/响应类型显示名。 + /// + private static bool TryCreateStreamInvokerRegistrationSpec( + INamedTypeSymbol handlerInterface, + out StreamInvokerRegistrationSpec streamInvokerRegistration) + { + if (!string.Equals( + handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + $"global::{CqrsContractsNamespace}.IStreamRequestHandler", + StringComparison.Ordinal)) + { + streamInvokerRegistration = default; + return false; + } + + if (handlerInterface.TypeArguments.Length != 2) + { + streamInvokerRegistration = default; + return false; + } + + streamInvokerRegistration = new StreamInvokerRegistrationSpec( + handlerInterface.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + handlerInterface.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + /// /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 /// CqrsHandlerRegistry.g.cs,并在需要时附带程序集级 reflection fallback 元数据。 diff --git a/GFramework.Cqrs.SourceGenerators/README.md b/GFramework.Cqrs.SourceGenerators/README.md index 1a77b07a..12e8f7c9 100644 --- a/GFramework.Cqrs.SourceGenerators/README.md +++ b/GFramework.Cqrs.SourceGenerators/README.md @@ -15,6 +15,7 @@ 并生成: - `ICqrsHandlerRegistry` 实现 +- 在运行时合同允许时,额外生成 request / stream invoker provider 与 descriptor 元数据 - 程序集级 `CqrsHandlerRegistryAttribute` - 必要时的 `CqrsReflectionFallbackAttribute` 元数据 @@ -34,6 +35,8 @@ 它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。 当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。 +当 runtime 同时暴露 request / stream invoker provider 契约时,生成注册器还会为可直接静态表达的 `IRequestHandler<,>` 与 +`IStreamRequestHandler<,>` 发射对应 descriptor 与开放静态 invoker 方法,让 runtime 在首次创建 request / stream binding 时优先消费这些编译期元数据;未命中时仍保持既有反射 binding 创建语义。 ## 最小接入路径 @@ -55,6 +58,7 @@ RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly); ``` 安装生成器后,运行时会优先走生成的 registry;无法静态表达的部分再走定向回退。 +如果当前 runtime 合同已经包含 request / stream invoker provider seam,generated registry 还会把这两类 invoker 元数据一并前移到编译期。 ## 什么时候值得安装 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index e5e5171f..b49e9d86 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -57,6 +57,24 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); } + /// + /// 验证 registrar 激活 generated registry 后,会把 stream invoker provider 注册到容器中。 + /// + [Test] + public void RegisterHandlers_Should_Register_Generated_Stream_Invoker_Provider() + { + var generatedAssembly = CreateGeneratedStreamInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var providers = container.GetAll(); + + Assert.That( + providers.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)])); + } + /// /// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。 /// @@ -74,6 +92,23 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(response, Is.EqualTo("generated:payload")); } + /// + /// 验证 dispatcher 在首次创建 stream binding 时,会优先消费 generated stream invoker provider。 + /// + [Test] + public async Task CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered() + { + var generatedAssembly = CreateGeneratedStreamInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))); + Assert.That(results, Is.EqualTo([30, 31])); + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// @@ -89,6 +124,21 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests return generatedAssembly; } + /// + /// 创建带有 generated stream invoker registry 元数据的程序集替身。 + /// + private static Mock CreateGeneratedStreamInvokerAssembly() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Cqrs.Tests.Cqrs.GeneratedStreamInvokerAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedStreamInvokerProviderRegistry))]); + return generatedAssembly; + } + /// /// 清空 registrar 静态缓存。 /// @@ -109,6 +159,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests ClearCache(GetDispatcherCacheField("RequestDispatchBindings")); ClearCache(GetDispatcherCacheField("StreamDispatchBindings")); ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers")); + ClearCache(GetDispatcherCacheField("GeneratedStreamInvokers")); } /// @@ -149,4 +200,22 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests .Invoke(cache, Array.Empty()); } + /// + /// 枚举并收集当前异步流中的全部元素,便于断言 generated stream invoker 的输出。 + /// + /// 流元素类型。 + /// 待消耗的异步流。 + /// 按产出顺序收集得到的元素列表。 + private static async Task> DrainAsync(IAsyncEnumerable stream) + { + ArgumentNullException.ThrowIfNull(stream); + + var items = new List(); + await foreach (var item in stream.ConfigureAwait(false)) + { + items.Add(item); + } + + return items; + } } diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs new file mode 100644 index 00000000..0159c7ce --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerProviderRegistry.cs @@ -0,0 +1,102 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 模拟同时提供 handler 注册与 stream invoker 元数据的 generated registry。 +/// +internal sealed class GeneratedStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors +{ + private static readonly CqrsStreamInvokerDescriptor Descriptor = new( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + Descriptor); + + /// + /// 将测试流式请求处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedStreamInvokerRequestHandler).FullName} as {typeof(IStreamRequestHandler).FullName}."); + } + + /// + /// 尝试返回指定 stream request/response 类型对对应的 generated invoker 描述符。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回的描述符。 + /// 若类型对匹配当前测试流式请求则返回 + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 返回当前 registry 暴露的全部 generated stream invoker 描述符。 + /// + /// 单条测试 stream invoker 描述符条目。 + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + /// + /// 模拟 generated stream invoker 直接执行后的返回值。 + /// + /// 当前流式请求处理器实例。 + /// 当前测试流式请求。 + /// 取消令牌。 + /// 带有 generated 语义的异步流,便于断言 dispatcher 走了 provider 路径。 + private static object InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + _ = handler as IStreamRequestHandler + ?? throw new InvalidOperationException("Generated stream invoker received an incompatible handler instance."); + var typedRequest = (GeneratedStreamInvokerRequest)request; + return StreamResultsAsync(typedRequest.Start, cancellationToken); + } + + /// + /// 构造供测试断言使用的固定异步流结果。 + /// + private static async IAsyncEnumerable StreamResultsAsync( + int start, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return start * 10; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + yield return start * 10 + 1; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs new file mode 100644 index 00000000..71c6fc56 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequest.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 用于验证 generated stream invoker provider 接线的测试流式请求。 +/// +/// 用于构造 generated stream 输出的起始值。 +internal sealed record GeneratedStreamInvokerRequest(int Start) : IStreamRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs new file mode 100644 index 00000000..f60a84ff --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedStreamInvokerRequestHandler.cs @@ -0,0 +1,34 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 供 generated stream invoker provider 测试使用的流式请求处理器。 +/// +internal sealed class GeneratedStreamInvokerRequestHandler : IStreamRequestHandler +{ + /// + /// 返回带有运行时处理器语义的异步流,便于和 generated invoker 自定义结果区分。 + /// + /// 当前测试流式请求。 + /// 取消令牌。 + /// 运行时处理器生成的异步流结果。 + public IAsyncEnumerable Handle(GeneratedStreamInvokerRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + return StreamResultsAsync(request.Start, cancellationToken); + } + + /// + /// 生成用于区分 runtime 路径的固定异步流结果。 + /// + private static async IAsyncEnumerable StreamResultsAsync( + int start, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return start; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + yield return start + 1; + } +} diff --git a/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs b/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs new file mode 100644 index 00000000..4f9f67e1 --- /dev/null +++ b/GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 描述单个 stream request/response 类型对在运行时建流时需要复用的元数据。 +/// +/// 当前流式请求处理器在容器中的服务类型。 +/// +/// 执行单个流式请求处理器的开放静态方法。 +/// dispatcher 会在首次创建 stream binding 时,把该方法绑定成内部使用的调用委托。 +/// +/// +/// dispatcher 仍会负责上下文注入; +/// 该描述符只前移流式请求处理器服务类型与直接调用方法元数据。 +/// +public sealed class CqrsStreamInvokerDescriptor( + 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/CqrsStreamInvokerDescriptorEntry.cs b/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs new file mode 100644 index 00000000..ffff9fb8 --- /dev/null +++ b/GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs @@ -0,0 +1,12 @@ +namespace GFramework.Cqrs; + +/// +/// 描述单个 stream request/response 类型对与其 generated invoker 元数据之间的映射条目。 +/// +/// 流式请求运行时类型。 +/// 流式响应元素类型。 +/// 对应的 generated stream invoker 描述符。 +public sealed record CqrsStreamInvokerDescriptorEntry( + Type RequestType, + Type ResponseType, + CqrsStreamInvokerDescriptor Descriptor); diff --git a/GFramework.Cqrs/ICqrsStreamInvokerProvider.cs b/GFramework.Cqrs/ICqrsStreamInvokerProvider.cs new file mode 100644 index 00000000..2a2d907e --- /dev/null +++ b/GFramework.Cqrs/ICqrsStreamInvokerProvider.cs @@ -0,0 +1,33 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 定义由源码生成器或手写注册器提供的 stream invoker 元数据契约。 +/// +/// +/// 该 seam 允许运行时在首次创建 stream dispatch binding 时, +/// 直接复用编译期已知的流式请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。 +/// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射 binding 创建路径。 +/// 当前默认 runtime 通过 在注册阶段一次性读取并缓存 +/// provider 暴露的描述符; +/// 主要用于 provider 自检、测试和显式调用场景,而不是 dispatcher 在建流热路径上的二次回调入口。 +/// +public interface ICqrsStreamInvokerProvider +{ + /// + /// 尝试为指定流式请求/响应类型对提供运行时元数据。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回的 stream invoker 元数据。 + /// 若当前 provider 可处理该流式请求/响应类型对则返回 ;否则返回 + /// + /// 若 provider 希望被默认 runtime 自动接线到 dispatcher 的 generated invoker 缓存中, + /// 还必须同时实现 ,以便 registrar 在注册阶段枚举全部描述符。 + /// + bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor); +} diff --git a/GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs b/GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs new file mode 100644 index 00000000..df849140 --- /dev/null +++ b/GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs; + +/// +/// 为 generated stream invoker provider 暴露可枚举描述符集合的内部辅助契约。 +/// +/// +/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 stream invoker 描述符, +/// 并把它们登记到 dispatcher 的进程级弱缓存中。 +/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。 +/// +public interface IEnumeratesCqrsStreamInvokerDescriptors +{ + /// + /// 返回当前 provider 可声明的全部 stream invoker 描述符条目。 + /// + /// 按 provider 定义顺序枚举的描述符条目集合。 + IReadOnlyList GetDescriptors(); +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index a88be732..17cbb67a 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -23,6 +23,11 @@ internal sealed class CqrsDispatcher( private static readonly WeakTypePairCache GeneratedRequestInvokers = new(); + // 卸载安全的进程级缓存:当 generated registry 提供 stream invoker 元数据时, + // registrar 会按流式请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。 + private static readonly WeakTypePairCache + GeneratedStreamInvokers = new(); + // 卸载安全的进程级缓存:通知类型只以弱键语义保留。 // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。 private static readonly WeakKeyCache @@ -276,11 +281,62 @@ internal sealed class CqrsDispatcher( /// private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType) { + var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor(requestType, responseType); + if (generatedDescriptor is not null) + { + var resolvedGeneratedDescriptor = generatedDescriptor.Value; + return new StreamDispatchBinding( + resolvedGeneratedDescriptor.HandlerType, + resolvedGeneratedDescriptor.Invoker); + } + return new StreamDispatchBinding( typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType), CreateStreamInvoker(requestType, responseType)); } + /// + /// 尝试从容器已注册的 generated stream invoker provider 中获取指定流式请求/响应类型对的元数据。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 命中时返回强类型化后的描述符;否则返回 + private static StreamInvokerDescriptor? TryGetGeneratedStreamInvokerDescriptor(Type requestType, Type responseType) + { + return GeneratedStreamInvokers.TryGetValue(requestType, responseType, out var metadata) && + metadata is not null + ? CreateStreamInvokerDescriptor(requestType, responseType, metadata) + : null; + } + + /// + /// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的 stream invoker 描述符。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// provider 返回的弱类型描述符。 + /// 可直接用于创建 stream dispatch binding 的描述符。 + /// 当 provider 返回的委托签名与当前流式请求/响应类型对不匹配时抛出。 + private static StreamInvokerDescriptor CreateStreamInvokerDescriptor( + Type requestType, + Type responseType, + GeneratedStreamInvokerMetadata descriptor) + { + if (!descriptor.InvokerMethod.IsStatic) + { + throw new InvalidOperationException( + $"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}."); + } + + if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}."); + } + + return new StreamInvokerDescriptor(descriptor.HandlerType, invoker); + } + /// /// 生成请求处理器调用委托,避免每次发送都重复反射。 /// @@ -646,6 +702,15 @@ internal sealed class CqrsDispatcher( Type HandlerType, MethodInfo InvokerMethod); + /// + /// 记录 registrar 写入的 generated stream invoker 元数据。 + /// + /// 流式请求处理器在容器中的服务类型。 + /// 执行流式请求处理器的开放静态方法。 + private sealed record GeneratedStreamInvokerMetadata( + Type HandlerType, + MethodInfo InvokerMethod); + /// /// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。 /// @@ -654,6 +719,15 @@ internal sealed class CqrsDispatcher( Type HandlerType, RequestInvoker Invoker); + /// + /// 保存 provider 返回的流式请求处理器服务类型与 stream invoker。 + /// + /// 流式请求处理器在容器中的服务类型。 + /// 执行流式请求处理器的调用委托。 + private readonly record struct StreamInvokerDescriptor( + Type HandlerType, + StreamInvoker Invoker); + /// /// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。 /// @@ -677,6 +751,29 @@ internal sealed class CqrsDispatcher( descriptor.InvokerMethod)); } + /// + /// 供 registrar 在 generated registry 激活后登记 stream invoker 元数据。 + /// + /// 流式请求运行时类型。 + /// 流式响应元素类型。 + /// 要登记的 generated stream invoker 描述符。 + internal static void RegisterGeneratedStreamInvokerDescriptor( + Type requestType, + Type responseType, + CqrsStreamInvokerDescriptor descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + ArgumentNullException.ThrowIfNull(descriptor); + + _ = GeneratedStreamInvokers.GetOrAdd( + requestType, + responseType, + (_, _) => new GeneratedStreamInvokerMetadata( + descriptor.HandlerType, + descriptor.InvokerMethod)); + } + /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index f6e88c1d..8284c912 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -240,6 +240,7 @@ internal static class CqrsHandlerRegistrar $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); registry.Register(services, logger); RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger); + RegisterGeneratedStreamInvokerProvider(services, registry, assemblyName, logger); } } @@ -298,6 +299,61 @@ internal static class CqrsHandlerRegistrar } } + /// + /// 当 generated registry 同时提供 stream invoker 元数据时,把该 provider 注册到当前容器中。 + /// + /// 目标服务集合。 + /// 当前已激活的 generated registry。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// provider 作为 registry 的附加能力注册到容器后,dispatcher 才能在首次建流时优先消费编译期生成的 invoker 元数据。 + /// 若 registry 不实现该契约,则保持现有纯反射 stream binding 创建语义。 + /// + private static void RegisterGeneratedStreamInvokerProvider( + IServiceCollection services, + ICqrsHandlerRegistry registry, + string assemblyName, + ILogger logger) + { + if (registry is not ICqrsStreamInvokerProvider provider) + return; + + RegisterGeneratedStreamInvokerDescriptors(provider, assemblyName, logger); + services.AddSingleton(typeof(ICqrsStreamInvokerProvider), provider); + logger.Debug( + $"Registered CQRS stream invoker provider {provider.GetType().FullName} for assembly {assemblyName}."); + } + + /// + /// 读取 generated stream invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。 + /// + /// 当前已激活的 stream invoker provider。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。 + /// 这样 stream dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。 + /// + private static void RegisterGeneratedStreamInvokerDescriptors( + ICqrsStreamInvokerProvider provider, + string assemblyName, + ILogger logger) + { + if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource) + return; + + foreach (var descriptorEntry in descriptorSource.GetDescriptors()) + { + CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor( + descriptorEntry.RequestType, + descriptorEntry.ResponseType, + descriptorEntry.Descriptor); + logger.Debug( + $"Registered generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from assembly {assemblyName}."); + } + } + /// /// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。 /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index ad8a93f7..c22b08e6 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2489,7 +2489,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + "global::System.Collections.Generic.IReadOnlyList global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()")); }); } @@ -2498,7 +2498,6 @@ public class CqrsHandlerRegistryGeneratorTests /// stream invoker 描述符与对应的开放静态 invoker 方法。 /// [Test] - [Ignore("Enable after generated stream invoker provider / descriptor emission lands in Phase 8.")] public void Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available() { var execution = ExecuteGenerator(StreamInvokerProviderSource); @@ -2549,7 +2548,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That( generatedSource, Does.Contain( - "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + "global::System.Collections.Generic.IReadOnlyList global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors.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 64a394db..ef965277 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-067` +- 恢复点编号:`CQRS-REWRITE-RP-068` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -49,6 +49,17 @@ CQRS 迁移与收敛。 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径 - `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义 - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 request invoker provider 成员与 invoker 方法 + - 已完成一轮 `dispatch/invoker` 生成前移的最小 stream 切片: + - `GFramework.Cqrs` 新增 `ICqrsStreamInvokerProvider`、`IEnumeratesCqrsStreamInvokerDescriptors`、 + `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` + - generated registry 若实现 stream invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器, + 并把 provider 枚举出的 stream invoker 描述符写入 dispatcher 的进程级弱缓存 + - `CqrsDispatcher` 现会在首次创建 stream dispatch binding 时优先命中 generated stream invoker 描述符; + 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 流式 binding 路径 + - `GFramework.Cqrs.Tests` 已扩充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated stream invoker 的最小语义 + - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 stream invoker provider 成员与 invoker 方法 + - `GFramework.Cqrs/README.md`、`GFramework.Cqrs.SourceGenerators/README.md`、`docs/zh-CN/core/cqrs.md` 与 + `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 现已同步说明 generated stream invoker 的接线与回退边界 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 @@ -235,6 +246,21 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;本轮确认 notification publisher seam、README 与文档更新未引入 `GFramework.Cqrs` 构建告警 +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 stream invoker provider 生成与显式枚举接口实现未引入生成器编译问题 +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 stream invoker provider fixture 与回归断言可以编译通过 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过 + - 备注:`4/4` passed;覆盖 generated request / stream invoker provider 的 registrar 接线与 dispatcher 消费语义 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`2/2` passed;确认 generated registry 会同时发射 request / stream invoker provider 描述符与静态 invoker 方法 +- `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh` + - 结果:通过 + - 备注:`1059` 个 tracked C# 文件命名校验全部通过;本轮新增 stream invoker 类型与测试命名未引入回归 - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 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 68ce00ac..51e5951b 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,62 @@ ## 2026-04-30 +### 阶段:generated stream invoker provider 最小落地(CQRS-REWRITE-RP-068) + +- 继续按 `gframework-batch-boot 50` 执行,基线仍为当前本地 `origin/main` +- 本轮开始前,`origin/main` 已追平到当前 `HEAD`;因此 branch diff 重新归零,主 stop condition 仍为“相对 `origin/main` 接近 `50 files`” +- 当前批次沿用上一轮 request invoker provider 的设计形状,只做 stream 路径的最小对称扩展,避免把 notification publisher seam、pipeline 或 telemetry 一并卷入 +- 本轮切片拆分: + - worker:`GFramework.Cqrs/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` + - worker:`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` + - 主线程:`GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`、 + `GFramework.Cqrs/*.cs` 新增 stream provider 契约、`GFramework.Cqrs.SourceGenerators/Cqrs/*`、 + `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` +- 主线程关键设计调整: + - 继续保持 dispatcher 的 stream binding 静态缓存只依赖 `requestType + responseType`,不回调具体容器实例 + - stream provider 与 request provider 一样在 registrar 注册阶段一次性枚举 descriptor,并写入 dispatcher 的进程级弱缓存 + - generated registry 同时实现 request 与 stream 两组 descriptor 枚举契约时,改用显式接口实现 `GetDescriptors()`,避免同名方法冲突 +- 已完成实现: + - `GFramework.Cqrs` 新增 `ICqrsStreamInvokerProvider`、`IEnumeratesCqrsStreamInvokerDescriptors`、 + `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` + - `CqrsHandlerRegistrar` 新增 stream provider 接线与 descriptor 登记路径 + - `CqrsDispatcher` 新增 generated stream invoker 弱缓存,并在 `CreateStream(...)` 首次创建 stream binding 时优先消费 generated stream invoker 元数据 + - `CqrsHandlerRegistryGenerator` 新增 stream invoker registration 建模、descriptor 发射、显式枚举接口实现与 `InvokeStreamHandler{n}(...)` 静态桥接方法 + - `GFramework.Cqrs.Tests` 新增 `GeneratedStreamInvokerProviderRegistry`、`GeneratedStreamInvokerRequest`、`GeneratedStreamInvokerRequestHandler`,并扩充 `CqrsGeneratedRequestInvokerProviderTests` + - `GFramework.Cqrs.SourceGenerators/README.md` 额外补齐模块级 README,对齐 generated stream invoker 语义 +- worker 产出已接受: + - 文档切片已把 request / stream invoker provider 作为并列 reader-facing 语义写入公开文档 + - generator 测试切片已补齐 stream invoker provider fixture 与断言;主线程根据最终实现把 request / stream 的 `GetDescriptors()` 断言统一收敛到显式接口实现版本 + +### 验证(RP-068) + +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"` + - 结果:通过,`4/4` 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|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过,`2/2` passed +- `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh` + - 结果:通过 +- `git diff --name-only origin/main...HEAD | wc -l` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `4 files` +- `git diff --numstat origin/main...HEAD` + - 结果:通过 + - 备注:当前相对 `origin/main` 的已提交 branch diff 为 `217 changed lines` + +### 当前下一步(RP-068) + +1. 在保持 branch diff 远低于 `50 files` 阈值的前提下,继续评估下一个低风险 `dispatch/invoker` 收敛切片 +2. 优先候选仍是 notification 路径是否值得引入同类 generated invoker seam,或继续补强 request / stream provider 的公开 API 入口与诊断语义 +3. 下一批落地前先提交当前 stream provider 批次,避免未提交改动持续堆叠 + ### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067) - 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 697656cc..e9b502ed 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -236,7 +236,7 @@ RegisterCqrsPipelineBehavior>(); | `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 请求、处理器和 runtime seam 的最小契约 | | `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase`、`QueryBase`、`NotificationBase`、`RequestBase`、`ContextAwareCqrsExtensions` | 业务侧常用基类和上下文发送入口 | | `GFramework.Cqrs/Cqrs/` | `AbstractCommandHandler<,>`、`AbstractQueryHandler<,>`、`AbstractRequestHandler<,>`、`AbstractStreamCommandHandler<,>`、`AbstractStreamQueryHandler<,>`、`LoggingBehavior<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 | -| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | +| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider`、`ICqrsStreamInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | | `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | ## 继续阅读 diff --git a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md index cce62f85..94c38765 100644 --- a/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md +++ b/docs/zh-CN/source-generators/cqrs-handler-registry-generator.md @@ -138,6 +138,7 @@ RegisterCqrsHandlersFromAssemblies( - `GFramework.Cqrs.CqrsHandlerRegistryAttribute` - `GFramework.Cqrs.CqrsReflectionFallbackAttribute` - `GFramework.Cqrs.ICqrsRequestInvokerProvider` +- `GFramework.Cqrs.ICqrsStreamInvokerProvider` - `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator` 模块族入口见: