mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
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:
parent
f17f9f3da6
commit
ea0b937705
@ -9,12 +9,17 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
string RequestTypeDisplayName,
|
string RequestTypeDisplayName,
|
||||||
string ResponseTypeDisplayName);
|
string ResponseTypeDisplayName);
|
||||||
|
|
||||||
|
private readonly record struct StreamInvokerRegistrationSpec(
|
||||||
|
string RequestTypeDisplayName,
|
||||||
|
string ResponseTypeDisplayName);
|
||||||
|
|
||||||
private readonly record struct HandlerRegistrationSpec(
|
private readonly record struct HandlerRegistrationSpec(
|
||||||
string HandlerInterfaceDisplayName,
|
string HandlerInterfaceDisplayName,
|
||||||
string ImplementationTypeDisplayName,
|
string ImplementationTypeDisplayName,
|
||||||
string HandlerInterfaceLogName,
|
string HandlerInterfaceLogName,
|
||||||
string ImplementationLogName,
|
string ImplementationLogName,
|
||||||
RequestInvokerRegistrationSpec? RequestInvokerRegistration);
|
RequestInvokerRegistrationSpec? RequestInvokerRegistration,
|
||||||
|
StreamInvokerRegistrationSpec? StreamInvokerRegistration);
|
||||||
|
|
||||||
private readonly record struct ReflectedImplementationRegistrationSpec(
|
private readonly record struct ReflectedImplementationRegistrationSpec(
|
||||||
string HandlerInterfaceDisplayName,
|
string HandlerInterfaceDisplayName,
|
||||||
@ -31,7 +36,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
bool HasReflectionTypeLookups,
|
bool HasReflectionTypeLookups,
|
||||||
bool HasExternalAssemblyTypeLookups,
|
bool HasExternalAssemblyTypeLookups,
|
||||||
bool SupportsRequestInvokerProvider,
|
bool SupportsRequestInvokerProvider,
|
||||||
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions)
|
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions,
|
||||||
|
bool SupportsStreamInvokerProvider,
|
||||||
|
ImmutableArray<StreamInvokerEmissionSpec> StreamInvokerEmissions)
|
||||||
{
|
{
|
||||||
public bool RequiresRegistryAssemblyVariable =>
|
public bool RequiresRegistryAssemblyVariable =>
|
||||||
HasReflectedImplementationRegistrations ||
|
HasReflectedImplementationRegistrations ||
|
||||||
@ -39,6 +46,8 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
HasReflectionTypeLookups;
|
HasReflectionTypeLookups;
|
||||||
|
|
||||||
public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty;
|
public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty;
|
||||||
|
|
||||||
|
public bool HasStreamInvokerProvider => SupportsStreamInvokerProvider && !StreamInvokerEmissions.IsDefaultOrEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RequestInvokerEmissionSpec(
|
private readonly record struct RequestInvokerEmissionSpec(
|
||||||
@ -47,6 +56,12 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
string HandlerInterfaceDisplayName,
|
string HandlerInterfaceDisplayName,
|
||||||
int MethodIndex);
|
int MethodIndex);
|
||||||
|
|
||||||
|
private readonly record struct StreamInvokerEmissionSpec(
|
||||||
|
string RequestTypeDisplayName,
|
||||||
|
string ResponseTypeDisplayName,
|
||||||
|
string HandlerInterfaceDisplayName,
|
||||||
|
int MethodIndex);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -328,5 +343,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
bool SupportsNamedReflectionFallbackTypes,
|
bool SupportsNamedReflectionFallbackTypes,
|
||||||
bool SupportsDirectReflectionFallbackTypes,
|
bool SupportsDirectReflectionFallbackTypes,
|
||||||
bool SupportsMultipleReflectionFallbackAttributes,
|
bool SupportsMultipleReflectionFallbackAttributes,
|
||||||
bool SupportsRequestInvokerProvider);
|
bool SupportsRequestInvokerProvider,
|
||||||
|
bool SupportsStreamInvokerProvider);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
var requestInvokerEmissions = CreateRequestInvokerEmissions(
|
var requestInvokerEmissions = CreateRequestInvokerEmissions(
|
||||||
generationEnvironment.SupportsRequestInvokerProvider,
|
generationEnvironment.SupportsRequestInvokerProvider,
|
||||||
registrations);
|
registrations);
|
||||||
|
var streamInvokerEmissions = CreateStreamInvokerEmissions(
|
||||||
|
generationEnvironment.SupportsStreamInvokerProvider,
|
||||||
|
registrations);
|
||||||
|
|
||||||
return new GeneratedRegistrySourceShape(
|
return new GeneratedRegistrySourceShape(
|
||||||
hasReflectedImplementationRegistrations,
|
hasReflectedImplementationRegistrations,
|
||||||
@ -64,7 +67,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
hasReflectionTypeLookups,
|
hasReflectionTypeLookups,
|
||||||
hasExternalAssemblyTypeLookups,
|
hasExternalAssemblyTypeLookups,
|
||||||
generationEnvironment.SupportsRequestInvokerProvider,
|
generationEnvironment.SupportsRequestInvokerProvider,
|
||||||
requestInvokerEmissions);
|
requestInvokerEmissions,
|
||||||
|
generationEnvironment.SupportsStreamInvokerProvider,
|
||||||
|
streamInvokerEmissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -111,6 +116,46 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
return builder.ToImmutable();
|
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>
|
/// <summary>
|
||||||
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
|
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -221,6 +266,15 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors");
|
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();
|
||||||
builder.AppendLine("{");
|
builder.AppendLine("{");
|
||||||
AppendRegisterMethod(builder, registrations, sourceShape);
|
AppendRegisterMethod(builder, registrations, sourceShape);
|
||||||
@ -231,6 +285,12 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions);
|
AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourceShape.HasStreamInvokerProvider)
|
||||||
|
{
|
||||||
|
builder.AppendLine();
|
||||||
|
AppendStreamInvokerProviderMembers(builder, sourceShape.StreamInvokerEmissions);
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceShape.HasExternalAssemblyTypeLookups)
|
if (sourceShape.HasExternalAssemblyTypeLookups)
|
||||||
{
|
{
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
@ -366,9 +426,11 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
|
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.Append(CqrsRuntimeNamespace);
|
||||||
builder.AppendLine(".CqrsRequestInvokerDescriptorEntry> GetDescriptors()");
|
builder.Append(".CqrsRequestInvokerDescriptorEntry> global::");
|
||||||
|
builder.Append(CqrsRuntimeNamespace);
|
||||||
|
builder.AppendLine(".IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()");
|
||||||
builder.AppendLine(" {");
|
builder.AppendLine(" {");
|
||||||
builder.AppendLine(" return RequestInvokerDescriptors;");
|
builder.AppendLine(" return RequestInvokerDescriptors;");
|
||||||
builder.AppendLine(" }");
|
builder.AppendLine(" }");
|
||||||
@ -424,6 +486,117 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
|||||||
builder.AppendLine(" }");
|
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(
|
private static void AppendDirectRegistrations(
|
||||||
StringBuilder builder,
|
StringBuilder builder,
|
||||||
ImplementationRegistrationSpec registration)
|
ImplementationRegistrationSpec registration)
|
||||||
|
|||||||
@ -22,6 +22,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor";
|
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor";
|
||||||
private const string CqrsRequestInvokerDescriptorEntryMetadataName =
|
private const string CqrsRequestInvokerDescriptorEntryMetadataName =
|
||||||
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry";
|
$"{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 =
|
private const string CqrsHandlerRegistryAttributeMetadataName =
|
||||||
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
||||||
@ -78,6 +85,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null &&
|
compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null &&
|
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null &&
|
||||||
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) 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 stringType = compilation.GetSpecialType(SpecialType.System_String);
|
||||||
var typeType = compilation.GetTypeByMetadataName("System.Type");
|
var typeType = compilation.GetTypeByMetadataName("System.Type");
|
||||||
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
|
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
|
||||||
@ -98,7 +110,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
supportsNamedReflectionFallbackTypes,
|
supportsNamedReflectionFallbackTypes,
|
||||||
supportsDirectReflectionFallbackTypes,
|
supportsDirectReflectionFallbackTypes,
|
||||||
supportsMultipleReflectionFallbackAttributes,
|
supportsMultipleReflectionFallbackAttributes,
|
||||||
supportsRequestInvokerProvider);
|
supportsRequestInvokerProvider,
|
||||||
|
supportsStreamInvokerProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||||
@ -234,6 +247,9 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
implementationLogName,
|
implementationLogName,
|
||||||
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
|
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
|
||||||
? requestInvokerRegistration
|
? requestInvokerRegistration
|
||||||
|
: null,
|
||||||
|
TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration)
|
||||||
|
? streamInvokerRegistration
|
||||||
: null));
|
: null));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -281,6 +297,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return true;
|
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>
|
/// <summary>
|
||||||
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
|
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
|
||||||
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
|
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
并生成:
|
并生成:
|
||||||
|
|
||||||
- `ICqrsHandlerRegistry` 实现
|
- `ICqrsHandlerRegistry` 实现
|
||||||
|
- 在运行时合同允许时,额外生成 request / stream invoker provider 与 descriptor 元数据
|
||||||
- 程序集级 `CqrsHandlerRegistryAttribute`
|
- 程序集级 `CqrsHandlerRegistryAttribute`
|
||||||
- 必要时的 `CqrsReflectionFallbackAttribute` 元数据
|
- 必要时的 `CqrsReflectionFallbackAttribute` 元数据
|
||||||
|
|
||||||
@ -34,6 +35,8 @@
|
|||||||
|
|
||||||
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
|
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
|
||||||
当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。
|
当 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;无法静态表达的部分再走定向回退。
|
安装生成器后,运行时会优先走生成的 registry;无法静态表达的部分再走定向回退。
|
||||||
|
如果当前 runtime 合同已经包含 request / stream invoker provider seam,generated registry 还会把这两类 invoker 元数据一并前移到编译期。
|
||||||
|
|
||||||
## 什么时候值得安装
|
## 什么时候值得安装
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,24 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
|
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>
|
/// <summary>
|
||||||
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
|
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -74,6 +92,23 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
Assert.That(response, Is.EqualTo("generated:payload"));
|
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>
|
/// <summary>
|
||||||
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -89,6 +124,21 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
return generatedAssembly;
|
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>
|
/// <summary>
|
||||||
/// 清空 registrar 静态缓存。
|
/// 清空 registrar 静态缓存。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -109,6 +159,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
ClearCache(GetDispatcherCacheField("RequestDispatchBindings"));
|
ClearCache(GetDispatcherCacheField("RequestDispatchBindings"));
|
||||||
ClearCache(GetDispatcherCacheField("StreamDispatchBindings"));
|
ClearCache(GetDispatcherCacheField("StreamDispatchBindings"));
|
||||||
ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers"));
|
ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers"));
|
||||||
|
ClearCache(GetDispatcherCacheField("GeneratedStreamInvokers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -149,4 +200,22 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
.Invoke(cache, Array.Empty<object>());
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs
Normal file
31
GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs
Normal 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));
|
||||||
|
}
|
||||||
12
GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs
Normal file
12
GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs
Normal 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);
|
||||||
33
GFramework.Cqrs/ICqrsStreamInvokerProvider.cs
Normal file
33
GFramework.Cqrs/ICqrsStreamInvokerProvider.cs
Normal 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);
|
||||||
|
}
|
||||||
18
GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs
Normal file
18
GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs
Normal 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();
|
||||||
|
}
|
||||||
@ -23,6 +23,11 @@ internal sealed class CqrsDispatcher(
|
|||||||
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
|
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
|
||||||
GeneratedRequestInvokers = new();
|
GeneratedRequestInvokers = new();
|
||||||
|
|
||||||
|
// 卸载安全的进程级缓存:当 generated registry 提供 stream invoker 元数据时,
|
||||||
|
// registrar 会按流式请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
|
||||||
|
private static readonly WeakTypePairCache<GeneratedStreamInvokerMetadata>
|
||||||
|
GeneratedStreamInvokers = new();
|
||||||
|
|
||||||
// 卸载安全的进程级缓存:通知类型只以弱键语义保留。
|
// 卸载安全的进程级缓存:通知类型只以弱键语义保留。
|
||||||
// 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
|
// 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
|
||||||
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
|
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
|
||||||
@ -276,11 +281,62 @@ internal sealed class CqrsDispatcher(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType)
|
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(
|
return new StreamDispatchBinding(
|
||||||
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType),
|
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType),
|
||||||
CreateStreamInvoker(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>
|
||||||
/// 生成请求处理器调用委托,避免每次发送都重复反射。
|
/// 生成请求处理器调用委托,避免每次发送都重复反射。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -646,6 +702,15 @@ internal sealed class CqrsDispatcher(
|
|||||||
Type HandlerType,
|
Type HandlerType,
|
||||||
MethodInfo InvokerMethod);
|
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>
|
/// <summary>
|
||||||
/// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。
|
/// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -654,6 +719,15 @@ internal sealed class CqrsDispatcher(
|
|||||||
Type HandlerType,
|
Type HandlerType,
|
||||||
RequestInvoker<TResponse> Invoker);
|
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>
|
/// <summary>
|
||||||
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
|
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -677,6 +751,29 @@ internal sealed class CqrsDispatcher(
|
|||||||
descriptor.InvokerMethod));
|
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>
|
/// <summary>
|
||||||
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
|
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
|
||||||
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。
|
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。
|
||||||
|
|||||||
@ -240,6 +240,7 @@ internal static class CqrsHandlerRegistrar
|
|||||||
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
|
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
|
||||||
registry.Register(services, logger);
|
registry.Register(services, logger);
|
||||||
RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger);
|
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>
|
/// <summary>
|
||||||
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -2489,7 +2489,7 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
generatedSource,
|
generatedSource,
|
||||||
Does.Contain(
|
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 方法。
|
/// stream invoker 描述符与对应的开放静态 invoker 方法。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[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()
|
public void Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available()
|
||||||
{
|
{
|
||||||
var execution = ExecuteGenerator(StreamInvokerProviderSource);
|
var execution = ExecuteGenerator(StreamInvokerProviderSource);
|
||||||
@ -2549,7 +2548,7 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
generatedSource,
|
generatedSource,
|
||||||
Does.Contain(
|
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()"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ CQRS 迁移与收敛。
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`CQRS-REWRITE-RP-067`
|
- 恢复点编号:`CQRS-REWRITE-RP-068`
|
||||||
- 当前阶段:`Phase 8`
|
- 当前阶段:`Phase 8`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
|
- 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
|
||||||
@ -49,6 +49,17 @@ CQRS 迁移与收敛。
|
|||||||
未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径
|
未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径
|
||||||
- `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义
|
- `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义
|
||||||
- `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 request invoker provider 成员与 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 拆分发射
|
- 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射
|
||||||
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
|
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
|
||||||
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
|
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
|
||||||
@ -235,6 +246,21 @@ CQRS 迁移与收敛。
|
|||||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||||
- 结果:通过
|
- 结果:通过
|
||||||
- 备注:`0 warning / 0 error`;本轮确认 notification publisher seam、README 与文档更新未引入 `GFramework.Cqrs` 构建告警
|
- 备注:`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`
|
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||||
- 结果:通过
|
- 结果:通过
|
||||||
- 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题
|
- 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题
|
||||||
|
|||||||
@ -2,6 +2,62 @@
|
|||||||
|
|
||||||
## 2026-04-30
|
## 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)
|
### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067)
|
||||||
|
|
||||||
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
|
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
|
||||||
|
|||||||
@ -236,7 +236,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
|||||||
| `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 请求、处理器和 runtime seam 的最小契约 |
|
| `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/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/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 发射与诊断边界 |
|
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 |
|
||||||
|
|
||||||
## 继续阅读
|
## 继续阅读
|
||||||
|
|||||||
@ -138,6 +138,7 @@ RegisterCqrsHandlersFromAssemblies(
|
|||||||
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
|
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
|
||||||
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
|
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
|
||||||
- `GFramework.Cqrs.ICqrsRequestInvokerProvider`
|
- `GFramework.Cqrs.ICqrsRequestInvokerProvider`
|
||||||
|
- `GFramework.Cqrs.ICqrsStreamInvokerProvider`
|
||||||
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
|
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
|
||||||
|
|
||||||
模块族入口见:
|
模块族入口见:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user