feat(cqrs): 补充生成式 stream invoker 接缝

- 新增 stream invoker provider、descriptor 与 dispatcher/registrar 接线

- 更新 source generator 与回归测试,覆盖 generated stream invoker 发射和消费语义

- 更新 CQRS 文档与 ai-plan 恢复点,补充 stream invoker 的接入与验证记录
This commit is contained in:
gewuyou 2026-04-30 13:26:54 +08:00
parent f17f9f3da6
commit ea0b937705
19 changed files with 792 additions and 12 deletions

View File

@ -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<RequestInvokerEmissionSpec> RequestInvokerEmissions)
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions,
bool SupportsStreamInvokerProvider,
ImmutableArray<StreamInvokerEmissionSpec> 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);
/// <summary>
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
/// </summary>
@ -328,5 +343,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
bool SupportsNamedReflectionFallbackTypes,
bool SupportsDirectReflectionFallbackTypes,
bool SupportsMultipleReflectionFallbackAttributes,
bool SupportsRequestInvokerProvider);
bool SupportsRequestInvokerProvider,
bool SupportsStreamInvokerProvider);
}

View File

@ -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);
}
/// <summary>
@ -111,6 +116,46 @@ public sealed partial class CqrsHandlerRegistryGenerator
return builder.ToImmutable();
}
/// <summary>
/// 从 direct handler 注册描述中提取 stream invoker 发射计划。
/// </summary>
/// <param name="supportsStreamInvokerProvider">
/// 指示当前 runtime 是否同时暴露 <c>ICqrsStreamInvokerProvider</c> 与
/// <c>IEnumeratesCqrsStreamInvokerDescriptors</c> 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。
/// </param>
/// <param name="registrations">已按稳定顺序整理完成的 handler 注册描述。</param>
/// <returns>
/// 由 <c>directRegistration.StreamInvokerRegistration</c> 派生出的 <see cref="StreamInvokerEmissionSpec" /> 集合。
/// <c>methodIndex</c> 按 <paramref name="registrations" /> 与其 direct registration 的遍历顺序单调递增,
/// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。
/// </returns>
private static ImmutableArray<StreamInvokerEmissionSpec> CreateStreamInvokerEmissions(
bool supportsStreamInvokerProvider,
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
if (!supportsStreamInvokerProvider)
return ImmutableArray<StreamInvokerEmissionSpec>.Empty;
var builder = ImmutableArray.CreateBuilder<StreamInvokerEmissionSpec>();
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();
}
/// <summary>
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
/// </summary>
@ -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
/// </remarks>
private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
{
builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::");
builder.Append(" global::System.Collections.Generic.IReadOnlyList<global::");
builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".CqrsRequestInvokerDescriptorEntry> 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(" }");
}
/// <summary>
/// 发射 generated registry 的 stream invoker provider 成员。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="streamInvokerEmissions">当前要输出的 stream invoker 发射计划。</param>
private static void AppendStreamInvokerProviderMembers(
StringBuilder builder,
ImmutableArray<StreamInvokerEmissionSpec> streamInvokerEmissions)
{
AppendStreamInvokerDescriptorArray(builder, streamInvokerEmissions);
builder.AppendLine();
AppendStreamInvokerProviderMethods(builder);
for (var index = 0; index < streamInvokerEmissions.Length; index++)
{
builder.AppendLine();
AppendStreamInvokerMethod(builder, streamInvokerEmissions[index]);
}
}
/// <summary>
/// 发射 generated registry 的 stream invoker 描述符数组。
/// </summary>
private static void AppendStreamInvokerDescriptorArray(
StringBuilder builder,
ImmutableArray<StreamInvokerEmissionSpec> 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(" ];");
}
/// <summary>
/// 发射 generated registry 对 stream invoker provider 契约的实现方法。
/// </summary>
private static void AppendStreamInvokerProviderMethods(StringBuilder builder)
{
builder.Append(" global::System.Collections.Generic.IReadOnlyList<global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsStreamInvokerDescriptorEntry> 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(" }");
}
/// <summary>
/// 为单个 stream invoker 描述符发射对应的静态强类型桥接方法。
/// </summary>
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)

View File

@ -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;
}
/// <summary>
/// 当当前直接注册项属于流式请求处理器时,提取 stream invoker provider 所需的请求/响应类型显示名。
/// </summary>
private static bool TryCreateStreamInvokerRegistrationSpec(
INamedTypeSymbol handlerInterface,
out StreamInvokerRegistrationSpec streamInvokerRegistration)
{
if (!string.Equals(
handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
$"global::{CqrsContractsNamespace}.IStreamRequestHandler<TRequest, TResponse>",
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;
}
/// <summary>
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。

View File

@ -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 seamgenerated registry 还会把这两类 invoker 元数据一并前移到编译期。
## 什么时候值得安装

View File

@ -57,6 +57,24 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
}
/// <summary>
/// 验证 registrar 激活 generated registry 后,会把 stream invoker provider 注册到容器中。
/// </summary>
[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<ICqrsStreamInvokerProvider>();
Assert.That(
providers.Select(static provider => provider.GetType()),
Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)]));
}
/// <summary>
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
/// </summary>
@ -74,6 +92,23 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(response, Is.EqualTo("generated:payload"));
}
/// <summary>
/// 验证 dispatcher 在首次创建 stream binding 时,会优先消费 generated stream invoker provider。
/// </summary>
[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]));
}
/// <summary>
/// 创建带有 generated request invoker registry 元数据的程序集替身。
/// </summary>
@ -89,6 +124,21 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
return generatedAssembly;
}
/// <summary>
/// 创建带有 generated stream invoker registry 元数据的程序集替身。
/// </summary>
private static Mock<Assembly> CreateGeneratedStreamInvokerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
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;
}
/// <summary>
/// 清空 registrar 静态缓存。
/// </summary>
@ -109,6 +159,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
ClearCache(GetDispatcherCacheField("RequestDispatchBindings"));
ClearCache(GetDispatcherCacheField("StreamDispatchBindings"));
ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers"));
ClearCache(GetDispatcherCacheField("GeneratedStreamInvokers"));
}
/// <summary>
@ -149,4 +200,22 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
.Invoke(cache, Array.Empty<object>());
}
/// <summary>
/// 枚举并收集当前异步流中的全部元素,便于断言 generated stream invoker 的输出。
/// </summary>
/// <typeparam name="TItem">流元素类型。</typeparam>
/// <param name="stream">待消耗的异步流。</param>
/// <returns>按产出顺序收集得到的元素列表。</returns>
private static async Task<IReadOnlyList<TItem>> DrainAsync<TItem>(IAsyncEnumerable<TItem> stream)
{
ArgumentNullException.ThrowIfNull(stream);
var items = new List<TItem>();
await foreach (var item in stream.ConfigureAwait(false))
{
items.Add(item);
}
return items;
}
}

View File

@ -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;
/// <summary>
/// 模拟同时提供 handler 注册与 stream invoker 元数据的 generated registry。
/// </summary>
internal sealed class GeneratedStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
typeof(GeneratedStreamInvokerRequest),
typeof(int),
Descriptor);
/// <summary>
/// 将测试流式请求处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedStreamInvokerRequestHandler).FullName} as {typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>).FullName}.");
}
/// <summary>
/// 尝试返回指定 stream request/response 类型对对应的 generated invoker 描述符。
/// </summary>
/// <param name="requestType">流式请求运行时类型。</param>
/// <param name="responseType">流式响应元素类型。</param>
/// <param name="descriptor">命中时返回的描述符。</param>
/// <returns>若类型对匹配当前测试流式请求则返回 <see langword="true" />。</returns>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 返回当前 registry 暴露的全部 generated stream invoker 描述符。
/// </summary>
/// <returns>单条测试 stream invoker 描述符条目。</returns>
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return [DescriptorEntry];
}
/// <summary>
/// 模拟 generated stream invoker 直接执行后的返回值。
/// </summary>
/// <param name="handler">当前流式请求处理器实例。</param>
/// <param name="request">当前测试流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>带有 generated 语义的异步流,便于断言 dispatcher 走了 provider 路径。</returns>
private static object InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
{
_ = handler as IStreamRequestHandler<GeneratedStreamInvokerRequest, int>
?? throw new InvalidOperationException("Generated stream invoker received an incompatible handler instance.");
var typedRequest = (GeneratedStreamInvokerRequest)request;
return StreamResultsAsync(typedRequest.Start, cancellationToken);
}
/// <summary>
/// 构造供测试断言使用的固定异步流结果。
/// </summary>
private static async IAsyncEnumerable<int> StreamResultsAsync(
int start,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return start * 10;
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
yield return start * 10 + 1;
}
}

View File

@ -0,0 +1,9 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 用于验证 generated stream invoker provider 接线的测试流式请求。
/// </summary>
/// <param name="Start">用于构造 generated stream 输出的起始值。</param>
internal sealed record GeneratedStreamInvokerRequest(int Start) : IStreamRequest<int>;

View File

@ -0,0 +1,34 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 供 generated stream invoker provider 测试使用的流式请求处理器。
/// </summary>
internal sealed class GeneratedStreamInvokerRequestHandler : IStreamRequestHandler<GeneratedStreamInvokerRequest, int>
{
/// <summary>
/// 返回带有运行时处理器语义的异步流,便于和 generated invoker 自定义结果区分。
/// </summary>
/// <param name="request">当前测试流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>运行时处理器生成的异步流结果。</returns>
public IAsyncEnumerable<int> Handle(GeneratedStreamInvokerRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
return StreamResultsAsync(request.Start, cancellationToken);
}
/// <summary>
/// 生成用于区分 runtime 路径的固定异步流结果。
/// </summary>
private static async IAsyncEnumerable<int> StreamResultsAsync(
int start,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return start;
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
yield return start + 1;
}
}

View File

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

View File

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

View File

@ -0,0 +1,33 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs;
/// <summary>
/// 定义由源码生成器或手写注册器提供的 stream invoker 元数据契约。
/// </summary>
/// <remarks>
/// 该 seam 允许运行时在首次创建 stream dispatch binding 时,
/// 直接复用编译期已知的流式请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。
/// 当当前程序集没有提供匹配项时dispatcher 仍会回退到既有的反射 binding 创建路径。
/// 当前默认 runtime 通过 <see cref="IEnumeratesCqrsStreamInvokerDescriptors" /> 在注册阶段一次性读取并缓存
/// provider 暴露的描述符;<see cref="TryGetDescriptor(Type, Type, out CqrsStreamInvokerDescriptor?)" />
/// 主要用于 provider 自检、测试和显式调用场景,而不是 dispatcher 在建流热路径上的二次回调入口。
/// </remarks>
public interface ICqrsStreamInvokerProvider
{
/// <summary>
/// 尝试为指定流式请求/响应类型对提供运行时元数据。
/// </summary>
/// <param name="requestType">流式请求运行时类型。</param>
/// <param name="responseType">流式响应元素类型。</param>
/// <param name="descriptor">命中时返回的 stream invoker 元数据。</param>
/// <returns>若当前 provider 可处理该流式请求/响应类型对则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
/// <remarks>
/// 若 provider 希望被默认 runtime 自动接线到 dispatcher 的 generated invoker 缓存中,
/// 还必须同时实现 <see cref="IEnumeratesCqrsStreamInvokerDescriptors" />,以便 registrar 在注册阶段枚举全部描述符。
/// </remarks>
bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor);
}

View File

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

View File

@ -23,6 +23,11 @@ internal sealed class CqrsDispatcher(
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
GeneratedRequestInvokers = new();
// 卸载安全的进程级缓存:当 generated registry 提供 stream invoker 元数据时,
// registrar 会按流式请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
private static readonly WeakTypePairCache<GeneratedStreamInvokerMetadata>
GeneratedStreamInvokers = new();
// 卸载安全的进程级缓存:通知类型只以弱键语义保留。
// 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
@ -276,11 +281,62 @@ internal sealed class CqrsDispatcher(
/// </summary>
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));
}
/// <summary>
/// 尝试从容器已注册的 generated stream invoker provider 中获取指定流式请求/响应类型对的元数据。
/// </summary>
/// <param name="requestType">流式请求运行时类型。</param>
/// <param name="responseType">流式响应元素类型。</param>
/// <returns>命中时返回强类型化后的描述符;否则返回 <see langword="null" />。</returns>
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;
}
/// <summary>
/// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的 stream invoker 描述符。
/// </summary>
/// <param name="requestType">流式请求运行时类型。</param>
/// <param name="responseType">流式响应元素类型。</param>
/// <param name="descriptor">provider 返回的弱类型描述符。</param>
/// <returns>可直接用于创建 stream dispatch binding 的描述符。</returns>
/// <exception cref="InvalidOperationException">当 provider 返回的委托签名与当前流式请求/响应类型对不匹配时抛出。</exception>
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);
}
/// <summary>
/// 生成请求处理器调用委托,避免每次发送都重复反射。
/// </summary>
@ -646,6 +702,15 @@ internal sealed class CqrsDispatcher(
Type HandlerType,
MethodInfo InvokerMethod);
/// <summary>
/// 记录 registrar 写入的 generated stream invoker 元数据。
/// </summary>
/// <param name="HandlerType">流式请求处理器在容器中的服务类型。</param>
/// <param name="InvokerMethod">执行流式请求处理器的开放静态方法。</param>
private sealed record GeneratedStreamInvokerMetadata(
Type HandlerType,
MethodInfo InvokerMethod);
/// <summary>
/// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。
/// </summary>
@ -654,6 +719,15 @@ internal sealed class CqrsDispatcher(
Type HandlerType,
RequestInvoker<TResponse> Invoker);
/// <summary>
/// 保存 provider 返回的流式请求处理器服务类型与 stream invoker。
/// </summary>
/// <param name="HandlerType">流式请求处理器在容器中的服务类型。</param>
/// <param name="Invoker">执行流式请求处理器的调用委托。</param>
private readonly record struct StreamInvokerDescriptor(
Type HandlerType,
StreamInvoker Invoker);
/// <summary>
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
/// </summary>
@ -677,6 +751,29 @@ internal sealed class CqrsDispatcher(
descriptor.InvokerMethod));
}
/// <summary>
/// 供 registrar 在 generated registry 激活后登记 stream invoker 元数据。
/// </summary>
/// <param name="requestType">流式请求运行时类型。</param>
/// <param name="responseType">流式响应元素类型。</param>
/// <param name="descriptor">要登记的 generated stream invoker 描述符。</param>
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));
}
/// <summary>
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。

View File

@ -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
}
}
/// <summary>
/// 当 generated registry 同时提供 stream invoker 元数据时,把该 provider 注册到当前容器中。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="registry">当前已激活的 generated registry。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <remarks>
/// provider 作为 registry 的附加能力注册到容器后dispatcher 才能在首次建流时优先消费编译期生成的 invoker 元数据。
/// 若 registry 不实现该契约,则保持现有纯反射 stream binding 创建语义。
/// </remarks>
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}.");
}
/// <summary>
/// 读取 generated stream invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。
/// </summary>
/// <param name="provider">当前已激活的 stream invoker provider。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <remarks>
/// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。
/// 这样 stream dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。
/// </remarks>
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}.");
}
}
/// <summary>
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
/// </summary>

View File

@ -2489,7 +2489,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.That(
generatedSource,
Does.Contain(
"public global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()"));
"global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()"));
});
}
@ -2498,7 +2498,6 @@ public class CqrsHandlerRegistryGeneratorTests
/// stream invoker 描述符与对应的开放静态 invoker 方法。
/// </summary>
[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<global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()"));
"global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors.GetDescriptors()"));
});
}

View File

@ -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` 模块构建问题

View File

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

View File

@ -236,7 +236,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
| `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime``ICqrsHandlerRegistrar``IPipelineBehavior<,>``IRequestHandler<,>``Unit` | 请求、处理器和 runtime seam 的最小契约 |
| `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase<TInput, TResponse>``QueryBase<TInput, TResponse>``NotificationBase<TInput>``RequestBase<TInput, TResponse>``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 发射与诊断边界 |
## 继续阅读

View File

@ -138,6 +138,7 @@ RegisterCqrsHandlersFromAssemblies(
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
- `GFramework.Cqrs.ICqrsRequestInvokerProvider`
- `GFramework.Cqrs.ICqrsStreamInvokerProvider`
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
模块族入口见: