Compare commits

..

3 Commits

Author SHA1 Message Date
gewuyou
a468c9b4cd docs(source-generators): 补充迁移兼容说明并归档恢复历史
- 补充 Schema 配置生成器专题的迁移步骤、兼容边界与回退建议

- 更新 active tracking 与 active trace,只保留当前恢复事实、验证结果与归档指针

- 新增 RP-049 到 RP-052 的状态、验证与时间线归档文件
2026-05-01 13:16:35 +08:00
gewuyou
097e97bcd6 docs(ai-plan): 更新文档覆盖恢复点
- 更新 committed diff 基线,记录本轮提交后的 8 files / 337 lines 状态

- 补充 documentation-full-coverage-governance 的下一步建议,避免后续按过时工作树峰值恢复
2026-04-30 16:27:52 +08:00
gewuyou
a92b37b022 docs(source-generators): 补充生成器专题覆盖并更新进度
- 新增 Schema 配置生成器专题页,补充输入契约、生成物与诊断边界

- 更新 source-generators、API 参考与 CQRS 文档,说明共享支撑层阅读路线与 fallback 分层

- 更新 documentation-full-coverage-governance 的 tracking 和 trace,记录批次指标与验证结果
2026-04-30 16:27:50 +08:00
33 changed files with 506 additions and 3106 deletions

View File

@ -9,23 +9,16 @@ 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,
string HandlerInterfaceLogName, string HandlerInterfaceLogName);
RequestInvokerRegistrationSpec? RequestInvokerRegistration,
StreamInvokerRegistrationSpec? StreamInvokerRegistration);
private readonly record struct OrderedRegistrationSpec( private readonly record struct OrderedRegistrationSpec(
string HandlerInterfaceLogName, string HandlerInterfaceLogName,
@ -38,9 +31,7 @@ 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 ||
@ -48,8 +39,6 @@ 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(
@ -58,12 +47,6 @@ 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>
@ -345,6 +328,5 @@ public sealed partial class CqrsHandlerRegistryGenerator
bool SupportsNamedReflectionFallbackTypes, bool SupportsNamedReflectionFallbackTypes,
bool SupportsDirectReflectionFallbackTypes, bool SupportsDirectReflectionFallbackTypes,
bool SupportsMultipleReflectionFallbackAttributes, bool SupportsMultipleReflectionFallbackAttributes,
bool SupportsRequestInvokerProvider, bool SupportsRequestInvokerProvider);
bool SupportsStreamInvokerProvider);
} }

View File

@ -57,9 +57,6 @@ 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,
@ -67,13 +64,11 @@ public sealed partial class CqrsHandlerRegistryGenerator
hasReflectionTypeLookups, hasReflectionTypeLookups,
hasExternalAssemblyTypeLookups, hasExternalAssemblyTypeLookups,
generationEnvironment.SupportsRequestInvokerProvider, generationEnvironment.SupportsRequestInvokerProvider,
requestInvokerEmissions, requestInvokerEmissions);
generationEnvironment.SupportsStreamInvokerProvider,
streamInvokerEmissions);
} }
/// <summary> /// <summary>
/// 从可直接表达 handler 接口的注册描述中提取 request invoker 发射计划。 /// 从 direct handler 注册描述中提取 request invoker 发射计划。
/// </summary> /// </summary>
/// <param name="supportsRequestInvokerProvider"> /// <param name="supportsRequestInvokerProvider">
/// 指示当前 runtime 是否同时暴露 <c>ICqrsRequestInvokerProvider</c> 与 /// 指示当前 runtime 是否同时暴露 <c>ICqrsRequestInvokerProvider</c> 与
@ -81,8 +76,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// </param> /// </param>
/// <param name="registrations">已按稳定顺序整理完成的 handler 注册描述。</param> /// <param name="registrations">已按稳定顺序整理完成的 handler 注册描述。</param>
/// <returns> /// <returns>
/// 由 direct registration 或 reflected-implementation registration 上的 /// 由 <c>directRegistration.RequestInvokerRegistration</c> 派生出的 <see cref="RequestInvokerEmissionSpec" /> 集合。
/// <c>RequestInvokerRegistration</c> 派生出的 <see cref="RequestInvokerEmissionSpec" /> 集合。
/// <c>methodIndex</c> 按 <paramref name="registrations" /> 与其 direct registration 的遍历顺序单调递增, /// <c>methodIndex</c> 按 <paramref name="registrations" /> 与其 direct registration 的遍历顺序单调递增,
/// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。
/// </returns> /// </returns>
@ -112,71 +106,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
directRegistration.HandlerInterfaceDisplayName, directRegistration.HandlerInterfaceDisplayName,
methodIndex++)); methodIndex++));
} }
foreach (var reflectedRegistration in registration.ReflectedImplementationRegistrations)
{
if (reflectedRegistration.RequestInvokerRegistration is not { } requestInvokerRegistration)
continue;
builder.Add(new RequestInvokerEmissionSpec(
requestInvokerRegistration.RequestTypeDisplayName,
requestInvokerRegistration.ResponseTypeDisplayName,
reflectedRegistration.HandlerInterfaceDisplayName,
methodIndex++));
}
}
return builder.ToImmutable();
}
/// <summary>
/// 从可直接表达 handler 接口的注册描述中提取 stream invoker 发射计划。
/// </summary>
/// <param name="supportsStreamInvokerProvider">
/// 指示当前 runtime 是否同时暴露 <c>ICqrsStreamInvokerProvider</c> 与
/// <c>IEnumeratesCqrsStreamInvokerDescriptors</c> 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。
/// </param>
/// <param name="registrations">已按稳定顺序整理完成的 handler 注册描述。</param>
/// <returns>
/// 由 direct registration 或 reflected-implementation registration 上的
/// <c>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++));
}
foreach (var reflectedRegistration in registration.ReflectedImplementationRegistrations)
{
if (reflectedRegistration.StreamInvokerRegistration is not { } streamInvokerRegistration)
continue;
builder.Add(new StreamInvokerEmissionSpec(
streamInvokerRegistration.RequestTypeDisplayName,
streamInvokerRegistration.ResponseTypeDisplayName,
reflectedRegistration.HandlerInterfaceDisplayName,
methodIndex++));
}
} }
return builder.ToImmutable(); return builder.ToImmutable();
@ -292,15 +221,6 @@ 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);
@ -311,12 +231,6 @@ 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();
@ -452,11 +366,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// </remarks> /// </remarks>
private static void AppendRequestInvokerProviderMethods(StringBuilder builder) private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
{ {
builder.Append(" global::System.Collections.Generic.IReadOnlyList<global::"); builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::");
builder.Append(CqrsRuntimeNamespace); builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsRequestInvokerDescriptorEntry> global::"); builder.AppendLine(".CqrsRequestInvokerDescriptorEntry> GetDescriptors()");
builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()");
builder.AppendLine(" {"); builder.AppendLine(" {");
builder.AppendLine(" return RequestInvokerDescriptors;"); builder.AppendLine(" return RequestInvokerDescriptors;");
builder.AppendLine(" }"); builder.AppendLine(" }");
@ -512,117 +424,6 @@ 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)

View File

@ -22,13 +22,6 @@ 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";
@ -85,11 +78,6 @@ 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 &&
@ -110,8 +98,7 @@ 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)
@ -247,9 +234,6 @@ 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;
} }
@ -258,13 +242,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
{ {
reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec(
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
GetLogDisplayName(handlerInterface), GetLogDisplayName(handlerInterface)));
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
? requestInvokerRegistration
: null,
TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration)
? streamInvokerRegistration
: null));
return true; return true;
} }
@ -303,34 +281,6 @@ 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 元数据。

View File

@ -15,7 +15,6 @@
并生成: 并生成:
- `ICqrsHandlerRegistry` 实现 - `ICqrsHandlerRegistry` 实现
- 在运行时合同允许时,额外生成 request / stream invoker provider 与 descriptor 元数据
- 程序集级 `CqrsHandlerRegistryAttribute` - 程序集级 `CqrsHandlerRegistryAttribute`
- 必要时的 `CqrsReflectionFallbackAttribute` 元数据 - 必要时的 `CqrsReflectionFallbackAttribute` 元数据
@ -35,8 +34,6 @@
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 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 创建语义。
## 最小接入路径 ## 最小接入路径
@ -58,7 +55,6 @@ RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly);
``` ```
安装生成器后,运行时会优先走生成的 registry无法静态表达的部分再走定向回退。 安装生成器后,运行时会优先走生成的 registry无法静态表达的部分再走定向回退。
如果当前 runtime 合同已经包含 request / stream invoker provider seamgenerated registry 还会把这两类 invoker 元数据一并前移到编译期。
## 什么时候值得安装 ## 什么时候值得安装

View File

@ -57,62 +57,6 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
} }
/// <summary>
/// 验证当实现类型隐藏、但 request handler interface 仍可直接表达时,
/// registrar 仍会把 generated request invoker provider 注册到容器中。
/// </summary>
[Test]
public void RegisterHandlers_Should_Register_Generated_Request_Invoker_Provider_For_Hidden_Implementation()
{
var generatedAssembly = CreateHiddenImplementationGeneratedRequestInvokerAssembly();
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
var providers = container.GetAll<ICqrsRequestInvokerProvider>();
Assert.That(
providers.Select(static provider => provider.GetType()),
Is.EqualTo([typeof(HiddenImplementationGeneratedRequestInvokerProviderRegistry)]));
}
/// <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>
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
/// registrar 仍会把 generated stream invoker provider 注册到容器中。
/// </summary>
[Test]
public void RegisterHandlers_Should_Register_Generated_Stream_Invoker_Provider_For_Hidden_Implementation()
{
var generatedAssembly = CreateHiddenImplementationGeneratedStreamInvokerAssembly();
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(HiddenImplementationGeneratedStreamInvokerProviderRegistry)]));
}
/// <summary> /// <summary>
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。 /// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
/// </summary> /// </summary>
@ -130,637 +74,6 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(response, Is.EqualTo("generated:payload")); Assert.That(response, Is.EqualTo("generated:payload"));
} }
/// <summary>
/// 验证当实现类型隐藏、但 request handler interface 仍可直接表达时,
/// dispatcher 仍会消费 generated request invoker descriptor。
/// </summary>
[Test]
public async Task SendAsync_Should_Use_Generated_Request_Invoker_For_Hidden_Implementation_When_Provider_Is_Registered()
{
var generatedAssembly = CreateHiddenImplementationGeneratedRequestInvokerAssembly();
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(
new HiddenImplementationRequestInvokerContainer.VisibleRequest("payload"));
Assert.That(response, Is.EqualTo("generated-hidden: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>
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
/// dispatcher 仍会消费 generated stream invoker descriptor。
/// </summary>
[Test]
public async Task CreateStream_Should_Use_Generated_Stream_Invoker_For_Hidden_Implementation_When_Provider_Is_Registered()
{
var generatedAssembly = CreateHiddenImplementationGeneratedStreamInvokerAssembly();
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var results = await DrainAsync(
context.CreateStream(new HiddenImplementationStreamInvokerContainer.VisibleStreamRequest(3)));
Assert.That(results, Is.EqualTo([300, 301]));
}
/// <summary>
/// 验证当 registry 只暴露 request invoker provider 接口、但不提供可枚举描述符契约时,
/// dispatcher 仍会回退到既有反射路径,而不是错误依赖未预热的 generated metadata。
/// </summary>
[Test]
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Provider_Does_Not_Enumerate_Descriptors()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(NonEnumeratingRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.NonEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("runtime:payload"));
}
/// <summary>
/// 验证当 registry 只暴露 stream invoker provider 接口、但不提供可枚举描述符契约时,
/// dispatcher 仍会回退到既有流式反射路径。
/// </summary>
[Test]
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Provider_Does_Not_Enumerate_Descriptors()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(NonEnumeratingStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.NonEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
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))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 验证当 generated request invoker provider 暴露实例方法时,
/// registrar 会放弃该 generated registry 并回退到运行时反射路径。
/// </summary>
[Test]
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Generated_Request_Invoker_Is_Not_Static()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(NonStaticRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.NonStaticRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("runtime:payload"));
}
/// <summary>
/// 验证当 generated request invoker provider 返回与 dispatcher 委托签名不兼容的方法时,
/// dispatcher 会显式抛出契约错误。
/// </summary>
[Test]
public void SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(IncompatibleRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.IncompatibleRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("incompatible invoker"));
}
/// <summary>
/// 验证当 generated stream invoker provider 暴露实例方法时,
/// registrar 会放弃该 generated registry 并回退到运行时反射路径。
/// </summary>
[Test]
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Generated_Stream_Invoker_Is_Not_Static()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(NonStaticStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.NonStaticStreamInvokerAssembly, Version=1.0.0.0");
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))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 验证当 generated stream invoker provider 返回与 dispatcher 委托签名不兼容的方法时,
/// dispatcher 会显式抛出契约错误。
/// </summary>
[Test]
public void CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(IncompatibleStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.IncompatibleStreamInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("incompatible invoker"));
}
/// <summary>
/// 验证当 generated request invoker provider 实现枚举契约、但返回空描述符集合时,
/// dispatcher 仍会回退到既有反射路径。
/// </summary>
[Test]
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Descriptor_Enumeration_Is_Empty()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(EmptyEnumeratingRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.EmptyEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("runtime:payload"));
}
/// <summary>
/// 验证当 generated stream invoker provider 实现枚举契约、但返回空描述符集合时,
/// dispatcher 仍会回退到既有流式反射路径。
/// </summary>
[Test]
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Descriptor_Enumeration_Is_Empty()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(EmptyEnumeratingStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.EmptyEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
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))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 模拟返回实例 request invoker 方法的 generated registry。
/// </summary>
private sealed class NonStaticRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = new CqrsRequestInvokerDescriptor(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(NonStaticRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Instance)!);
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsRequestInvokerDescriptorEntry(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
new CqrsRequestInvokerDescriptor(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(NonStaticRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Instance)!))
];
}
private ValueTask<string> InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(string.Empty);
}
}
/// <summary>
/// 模拟返回不兼容 request invoker 方法的 generated registry。
/// </summary>
private sealed class IncompatibleRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
new CqrsRequestInvokerDescriptor(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(IncompatibleRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!));
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = DescriptorEntry.Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return [DescriptorEntry];
}
private static string InvokeGenerated(object handler, object request)
{
return string.Empty;
}
}
/// <summary>
/// 模拟返回实例 stream invoker 方法的 generated registry。
/// </summary>
private sealed class NonStaticStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = new CqrsStreamInvokerDescriptor(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(NonStaticStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Instance)!);
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsStreamInvokerDescriptorEntry(
typeof(GeneratedStreamInvokerRequest),
typeof(int),
new CqrsStreamInvokerDescriptor(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(NonStaticStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Instance)!))
];
}
private object InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
{
return Array.Empty<int>().ToAsyncEnumerable();
}
}
/// <summary>
/// 模拟返回不兼容 stream invoker 方法的 generated registry。
/// </summary>
private sealed class IncompatibleStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
typeof(GeneratedStreamInvokerRequest),
typeof(int),
new CqrsStreamInvokerDescriptor(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(IncompatibleStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!));
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = DescriptorEntry.Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return [DescriptorEntry];
}
private static object InvokeGenerated(object handler, object request)
{
return Array.Empty<int>().ToAsyncEnumerable();
}
}
/// <summary>
/// 模拟只暴露 request provider 接口、但不暴露描述符枚举契约的 generated registry。
/// </summary>
private sealed class NonEnumeratingRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider
{
private static readonly CqrsRequestInvokerDescriptor Descriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
}
/// <summary>
/// 模拟只暴露 stream provider 接口、但不暴露描述符枚举契约的 generated registry。
/// </summary>
private sealed class NonEnumeratingStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider
{
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
}
/// <summary>
/// 模拟实现 request descriptor 枚举契约、但当前不暴露任何 descriptor 的 generated registry。
/// </summary>
private sealed class EmptyEnumeratingRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return Array.Empty<CqrsRequestInvokerDescriptorEntry>();
}
}
/// <summary>
/// 模拟实现 stream descriptor 枚举契约、但当前不暴露任何 descriptor 的 generated registry。
/// </summary>
private sealed class EmptyEnumeratingStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return Array.Empty<CqrsStreamInvokerDescriptorEntry>();
}
}
/// <summary> /// <summary>
/// 创建带有 generated request invoker registry 元数据的程序集替身。 /// 创建带有 generated request invoker registry 元数据的程序集替身。
/// </summary> /// </summary>
@ -776,72 +89,6 @@ 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>
/// 创建带有指定 generated registry 元数据的程序集替身。
/// </summary>
/// <param name="registryType">测试 registry 类型。</param>
/// <param name="assemblyFullName">模拟程序集全名。</param>
/// <returns>可用于 registrar 注册流程的程序集替身。</returns>
private static Mock<Assembly> CreateGeneratedAssembly(Type registryType, string assemblyFullName)
{
ArgumentNullException.ThrowIfNull(registryType);
ArgumentException.ThrowIfNullOrWhiteSpace(assemblyFullName);
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns(assemblyFullName);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(registryType)]);
return generatedAssembly;
}
/// <summary>
/// 创建带有 hidden implementation request invoker registry 元数据的程序集替身。
/// </summary>
private static Mock<Assembly> CreateHiddenImplementationGeneratedRequestInvokerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Cqrs.Tests.Cqrs.HiddenGeneratedRequestInvokerAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(HiddenImplementationGeneratedRequestInvokerProviderRegistry))]);
return generatedAssembly;
}
/// <summary>
/// 创建带有 hidden implementation stream invoker registry 元数据的程序集替身。
/// </summary>
private static Mock<Assembly> CreateHiddenImplementationGeneratedStreamInvokerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Cqrs.Tests.Cqrs.HiddenGeneratedStreamInvokerAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(HiddenImplementationGeneratedStreamInvokerProviderRegistry))]);
return generatedAssembly;
}
/// <summary> /// <summary>
/// 清空 registrar 静态缓存。 /// 清空 registrar 静态缓存。
/// </summary> /// </summary>
@ -862,7 +109,6 @@ 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>
@ -903,22 +149,4 @@ 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;
}
} }

View File

@ -1,102 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,34 +0,0 @@
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

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

View File

@ -1,114 +0,0 @@
using System.Reflection;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 模拟 generated registry 在实现类型隐藏、但 stream handler interface 可见时,仍提供 stream invoker 元数据。
/// </summary>
internal sealed class HiddenImplementationGeneratedStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly Type HandlerContractType =
typeof(IStreamRequestHandler<HiddenImplementationStreamInvokerContainer.VisibleStreamRequest, int>);
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
HandlerContractType,
typeof(HiddenImplementationGeneratedStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
typeof(HiddenImplementationStreamInvokerContainer.VisibleStreamRequest),
typeof(int),
Descriptor);
/// <summary>
/// 通过可见 stream handler interface 把隐藏实现类型注册进目标服务集合,模拟 generator 的 reflected-implementation 路径。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="services" /> 或 <paramref name="logger" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
var implementationType = HiddenImplementationStreamInvokerContainer.HiddenHandlerType;
services.AddTransient(HandlerContractType, implementationType);
logger.Debug(
$"Registered CQRS handler {implementationType.FullName} as {HandlerContractType.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>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="requestType" /> 或 <paramref name="responseType" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(HiddenImplementationStreamInvokerContainer.VisibleStreamRequest)
&& responseType == typeof(int))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 返回当前 registry 暴露的全部 generated stream invoker 描述符。
/// </summary>
/// <returns>单条 hidden implementation 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>带有 hidden generated 语义的异步流,便于断言 dispatcher 命中了 generated provider 路径。</returns>
private static object InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
{
_ = handler as IStreamRequestHandler<HiddenImplementationStreamInvokerContainer.VisibleStreamRequest, int>
?? throw new InvalidOperationException("Generated stream invoker received an incompatible hidden handler instance.");
var typedRequest = (HiddenImplementationStreamInvokerContainer.VisibleStreamRequest)request;
return StreamResultsAsync(typedRequest.Start, cancellationToken);
}
/// <summary>
/// 构造供测试断言使用的固定异步流结果。
/// </summary>
private static async IAsyncEnumerable<int> StreamResultsAsync(
int start,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return start * 100;
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
yield return start * 100 + 1;
}
}

View File

@ -1,38 +0,0 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 hidden implementation request invoker 回归提供“可见请求 + 隐藏实现类型”的测试替身容器。
/// </summary>
internal static class HiddenImplementationRequestInvokerContainer
{
/// <summary>
/// 用于验证 generated request invoker metadata 在隐藏实现类型场景下仍可被 dispatcher 消费的请求。
/// </summary>
/// <param name="Value">用于断言 generated 返回值的请求负载。</param>
internal sealed record VisibleRequest(string Value) : IRequest<string>;
/// <summary>
/// 供 registrar 通过可见 handler interface 注册、但自身保持隐藏的 request handler 实现。
/// </summary>
private sealed class HiddenHandler : IRequestHandler<VisibleRequest, string>
{
/// <summary>
/// 返回 runtime 路径专用结果,便于与 generated invoker 路径区分。
/// </summary>
/// <param name="request">当前测试请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>runtime handler 生成的响应字符串。</returns>
public ValueTask<string> Handle(VisibleRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
return ValueTask.FromResult($"runtime-hidden:{request.Value}");
}
}
/// <summary>
/// 返回当前隐藏 request handler 实现类型,供 generated registry 以反射注册语义模拟 hidden implementation 场景。
/// </summary>
internal static Type HiddenHandlerType => typeof(HiddenHandler);
}

View File

@ -1,51 +0,0 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 hidden implementation stream invoker 回归提供“可见请求 + 隐藏实现类型”的测试替身容器。
/// </summary>
internal static class HiddenImplementationStreamInvokerContainer
{
/// <summary>
/// 用于验证 generated stream invoker metadata 在隐藏实现类型场景下仍可被 dispatcher 消费的流式请求。
/// </summary>
/// <param name="Start">用于构造 generated stream 输出的起始值。</param>
internal sealed record VisibleStreamRequest(int Start) : IStreamRequest<int>;
/// <summary>
/// 供 registrar 通过可见 stream handler interface 注册、但自身保持隐藏的流式 handler 实现。
/// </summary>
private sealed class HiddenHandler : IStreamRequestHandler<VisibleStreamRequest, int>
{
/// <summary>
/// 返回 runtime 路径专用异步流,便于与 generated invoker 路径区分。
/// </summary>
/// <param name="request">当前测试流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>runtime handler 生成的异步流结果。</returns>
public IAsyncEnumerable<int> Handle(VisibleStreamRequest 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;
}
}
/// <summary>
/// 返回当前隐藏 stream handler 实现类型,供 generated registry 以反射注册语义模拟 hidden implementation 场景。
/// </summary>
internal static Type HiddenHandlerType => typeof(HiddenHandler);
}

View File

@ -19,9 +19,6 @@ public sealed class CqrsRequestInvokerDescriptor(
Type handlerType, Type handlerType,
MethodInfo invokerMethod) MethodInfo invokerMethod)
{ {
private static readonly string NonStaticInvokerMessage =
"CQRS request invoker descriptors require an open static invoker method so generated metadata can be bound deterministically.";
/// <summary> /// <summary>
/// 获取请求处理器在容器中的服务类型。 /// 获取请求处理器在容器中的服务类型。
/// </summary> /// </summary>
@ -30,22 +27,5 @@ public sealed class CqrsRequestInvokerDescriptor(
/// <summary> /// <summary>
/// 获取执行请求处理器的开放静态方法。 /// 获取执行请求处理器的开放静态方法。
/// </summary> /// </summary>
public MethodInfo InvokerMethod { get; } = ValidateInvokerMethod(invokerMethod); public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod));
/// <summary>
/// 在描述符构造阶段拒绝实例方法,避免非法 generated metadata 延迟到首次分发时才暴露。
/// </summary>
/// <param name="invokerMethod">待验证的 generated invoker 方法。</param>
/// <returns>通过校验的静态方法。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="invokerMethod" /> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="invokerMethod" /> 不是静态方法时抛出。</exception>
private static MethodInfo ValidateInvokerMethod(MethodInfo invokerMethod)
{
ArgumentNullException.ThrowIfNull(invokerMethod);
if (!invokerMethod.IsStatic)
throw new ArgumentException(NonStaticInvokerMessage, nameof(invokerMethod));
return invokerMethod;
}
} }

View File

@ -3,39 +3,10 @@ namespace GFramework.Cqrs;
/// <summary> /// <summary>
/// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。 /// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。
/// </summary> /// </summary>
public sealed record CqrsRequestInvokerDescriptorEntry /// <param name="RequestType">请求运行时类型。</param>
{ /// <param name="ResponseType">响应运行时类型。</param>
/// <summary> /// <param name="Descriptor">对应的 generated request invoker 描述符。</param>
/// 初始化 request invoker 描述符映射条目。 public sealed record CqrsRequestInvokerDescriptorEntry(
/// </summary> Type RequestType,
/// <param name="requestType">请求运行时类型。</param> Type ResponseType,
/// <param name="responseType">响应运行时类型。</param> CqrsRequestInvokerDescriptor Descriptor);
/// <param name="descriptor">对应的 generated request invoker 描述符。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="requestType" />、<paramref name="responseType" /> 或 <paramref name="descriptor" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public CqrsRequestInvokerDescriptorEntry(
Type requestType,
Type responseType,
CqrsRequestInvokerDescriptor descriptor)
{
RequestType = requestType ?? throw new ArgumentNullException(nameof(requestType));
ResponseType = responseType ?? throw new ArgumentNullException(nameof(responseType));
Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
}
/// <summary>
/// 获取请求运行时类型。
/// </summary>
public Type RequestType { get; }
/// <summary>
/// 获取响应运行时类型。
/// </summary>
public Type ResponseType { get; }
/// <summary>
/// 获取对应的 generated request invoker 描述符。
/// </summary>
public CqrsRequestInvokerDescriptor Descriptor { get; }
}

View File

@ -1,51 +0,0 @@
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)
{
private static readonly string NonStaticInvokerMessage =
"CQRS stream invoker descriptors require an open static invoker method so generated metadata can be bound deterministically.";
/// <summary>
/// 获取流式请求处理器在容器中的服务类型。
/// </summary>
public Type HandlerType { get; } = handlerType ?? throw new ArgumentNullException(nameof(handlerType));
/// <summary>
/// 获取执行流式请求处理器的开放静态方法。
/// </summary>
public MethodInfo InvokerMethod { get; } = ValidateInvokerMethod(invokerMethod);
/// <summary>
/// 在描述符构造阶段拒绝实例方法,避免非法 generated metadata 延迟到首次建流时才暴露。
/// </summary>
/// <param name="invokerMethod">待验证的 generated invoker 方法。</param>
/// <returns>通过校验的静态方法。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="invokerMethod" /> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="invokerMethod" /> 不是静态方法时抛出。</exception>
private static MethodInfo ValidateInvokerMethod(MethodInfo invokerMethod)
{
ArgumentNullException.ThrowIfNull(invokerMethod);
if (!invokerMethod.IsStatic)
throw new ArgumentException(NonStaticInvokerMessage, nameof(invokerMethod));
return invokerMethod;
}
}

View File

@ -1,41 +0,0 @@
namespace GFramework.Cqrs;
/// <summary>
/// 描述单个 stream request/response 类型对与其 generated invoker 元数据之间的映射条目。
/// </summary>
public sealed record CqrsStreamInvokerDescriptorEntry
{
/// <summary>
/// 初始化 stream invoker 描述符映射条目。
/// </summary>
/// <param name="requestType">流式请求运行时类型。</param>
/// <param name="responseType">流式响应元素类型。</param>
/// <param name="descriptor">对应的 generated stream invoker 描述符。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="requestType" />、<paramref name="responseType" /> 或 <paramref name="descriptor" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public CqrsStreamInvokerDescriptorEntry(
Type requestType,
Type responseType,
CqrsStreamInvokerDescriptor descriptor)
{
RequestType = requestType ?? throw new ArgumentNullException(nameof(requestType));
ResponseType = responseType ?? throw new ArgumentNullException(nameof(responseType));
Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
}
/// <summary>
/// 获取流式请求运行时类型。
/// </summary>
public Type RequestType { get; }
/// <summary>
/// 获取流式响应元素类型。
/// </summary>
public Type ResponseType { get; }
/// <summary>
/// 获取对应的 generated stream invoker 描述符。
/// </summary>
public CqrsStreamInvokerDescriptor Descriptor { get; }
}

View File

@ -1,33 +0,0 @@
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

@ -1,18 +0,0 @@
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,11 +23,6 @@ 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>
@ -256,23 +251,14 @@ internal sealed class CqrsDispatcher(
$"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); $"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
} }
try if (Delegate.CreateDelegate(typeof(RequestInvoker<TResponse>), descriptor.InvokerMethod) is not
{ RequestInvoker<TResponse> invoker)
if (Delegate.CreateDelegate(typeof(RequestInvoker<TResponse>), descriptor.InvokerMethod) is not
RequestInvoker<TResponse> invoker)
{
throw new InvalidOperationException(
$"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
}
return new RequestInvokerDescriptor<TResponse>(descriptor.HandlerType, invoker);
}
catch (ArgumentException exception)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.", $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
exception);
} }
return new RequestInvokerDescriptor<TResponse>(descriptor.HandlerType, invoker);
} }
/// <summary> /// <summary>
@ -290,71 +276,11 @@ 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}.");
}
try
{
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);
}
catch (ArgumentException exception)
{
throw new InvalidOperationException(
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.",
exception);
}
}
/// <summary> /// <summary>
/// 生成请求处理器调用委托,避免每次发送都重复反射。 /// 生成请求处理器调用委托,避免每次发送都重复反射。
/// </summary> /// </summary>
@ -720,15 +646,6 @@ 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>
@ -737,15 +654,6 @@ 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>
@ -769,29 +677,6 @@ 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 缓存。
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。

View File

@ -240,7 +240,6 @@ 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);
} }
} }
@ -299,61 +298,6 @@ 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>

View File

@ -20,7 +20,7 @@
- `GeWuYou.GFramework.Cqrs` - `GeWuYou.GFramework.Cqrs`
- 默认 runtime 与业务侧常用基类。 - 默认 runtime 与业务侧常用基类。
- `GeWuYou.GFramework.Cqrs.SourceGenerators` - `GeWuYou.GFramework.Cqrs.SourceGenerators`
- 可选。为消费端程序集生成 `ICqrsHandlerRegistry`并在可用时补充 generated request / stream invoker provider 元数据;运行时会优先消费这些编译期元数据,只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。 - 可选。为消费端程序集生成 `ICqrsHandlerRegistry`运行时优先走生成注册表;只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。
- `GFramework.Core` - `GFramework.Core`
- 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。 - 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。
@ -120,15 +120,13 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
## 运行时行为 ## 运行时行为
- 请求分发 - 请求分发
- `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,若当前程序集提供 generated request invoker provider则会先复用对应 descriptor 中的处理器服务类型与 invoker 元数据;未命中时仍回退到既有反射 request binding 创建路径。 - `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,未找到处理器会抛出异常。
- 未找到处理器会抛出异常。
- 通知分发 - 通知分发
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。 - 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
- 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。 - 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。 - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。
- 流式请求 - 流式请求
- 通过 `IStreamRequest<TResponse>``IStreamRequestHandler<,>` 返回 `IAsyncEnumerable<TResponse>` - 通过 `IStreamRequest<TResponse>``IStreamRequestHandler<,>` 返回 `IAsyncEnumerable<TResponse>`
- 当消费端程序集提供 generated stream invoker provider / descriptor 后runtime 会优先消费这组 stream invoker 元数据;未命中时仍回退到既有反射 stream binding 创建路径。
- 上下文注入 - 上下文注入
- 处理器基类继承 `CqrsContextAwareHandlerBase`runtime 会在分发前注入当前 `IArchitectureContext` - 处理器基类继承 `CqrsContextAwareHandlerBase`runtime 会在分发前注入当前 `IArchitectureContext`
- 如果处理器或行为需要上下文注入,而当前 `ICqrsContext` 不是 `IArchitectureContext`,默认实现会抛出异常。 - 如果处理器或行为需要上下文注入,而当前 `ICqrsContext` 不是 `IArchitectureContext`,默认实现会抛出异常。
@ -142,7 +140,6 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
- 同一程序集按稳定键去重,避免重复注册。 - 同一程序集按稳定键去重,避免重复注册。
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。 - 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时registrar 会把对应 descriptor 元数据接线到 runtime 缓存。
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。 - 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。 - 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]``string[]` 两类 fallback 清单。 - `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]``string[]` 两类 fallback 清单。

View File

@ -1790,534 +1790,6 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
private const string StreamInvokerProviderSource = """
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
{
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse>
{
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
public interface ICqrsStreamInvokerProvider
{
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor);
}
public interface IEnumeratesCqrsStreamInvokerDescriptors
{
IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors();
}
public sealed class CqrsStreamInvokerDescriptor
{
public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
}
public sealed class CqrsStreamInvokerDescriptorEntry
{
public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor)
{
RequestType = requestType;
ResponseType = responseType;
Descriptor = descriptor;
}
public Type RequestType { get; }
public Type ResponseType { get; }
public CqrsStreamInvokerDescriptor Descriptor { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleStream(int Count) : IStreamRequest<int>;
public sealed class VisibleStreamHandler : IStreamRequestHandler<VisibleStream, int>
{
public async IAsyncEnumerable<int> Handle(VisibleStream request, CancellationToken cancellationToken)
{
yield return request.Count;
await Task.CompletedTask;
}
}
}
""";
private const string HiddenImplementationRequestInvokerProviderSource = """
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
{
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
public interface ICqrsRequestInvokerProvider
{
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor);
}
public interface IEnumeratesCqrsRequestInvokerDescriptors
{
IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors();
}
public sealed class CqrsRequestInvokerDescriptor
{
public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
}
public sealed class CqrsRequestInvokerDescriptorEntry
{
public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor)
{
RequestType = requestType;
ResponseType = responseType;
Descriptor = descriptor;
}
public Type RequestType { get; }
public Type ResponseType { get; }
public CqrsRequestInvokerDescriptor Descriptor { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleRequest() : IRequest<string>;
public sealed class Container
{
private sealed class HiddenHandler : IRequestHandler<VisibleRequest, string>
{
public ValueTask<string> Handle(VisibleRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(string.Empty);
}
}
}
}
""";
private const string HiddenImplementationStreamInvokerProviderSource = """
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse>
{
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
public interface ICqrsStreamInvokerProvider
{
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor);
}
public interface IEnumeratesCqrsStreamInvokerDescriptors
{
IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors();
}
public sealed class CqrsStreamInvokerDescriptor
{
public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
}
public sealed class CqrsStreamInvokerDescriptorEntry
{
public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor)
{
RequestType = requestType;
ResponseType = responseType;
Descriptor = descriptor;
}
public Type RequestType { get; }
public Type ResponseType { get; }
public CqrsStreamInvokerDescriptor Descriptor { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleStream() : IStreamRequest<int>;
public sealed class Container
{
private sealed class HiddenHandler : IStreamRequestHandler<VisibleStream, int>
{
public async IAsyncEnumerable<int> Handle(VisibleStream request, CancellationToken cancellationToken)
{
yield return 1;
await Task.CompletedTask;
}
}
}
}
""";
private const string PreciseReflectedRequestInvokerProviderBoundarySource = """
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
{
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
public interface ICqrsRequestInvokerProvider
{
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor);
}
public interface IEnumeratesCqrsRequestInvokerDescriptors
{
IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors();
}
public sealed class CqrsRequestInvokerDescriptor
{
public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
}
public sealed class CqrsRequestInvokerDescriptorEntry
{
public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor)
{
RequestType = requestType;
ResponseType = responseType;
Descriptor = descriptor;
}
public Type RequestType { get; }
public Type ResponseType { get; }
public CqrsRequestInvokerDescriptor Descriptor { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed class Container
{
private sealed record HiddenResponse();
private sealed record HiddenRequest() : IRequest<HiddenResponse[]>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, HiddenResponse[]>
{
public ValueTask<HiddenResponse[]> Handle(HiddenRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(Array.Empty<HiddenResponse>());
}
}
}
}
""";
private const string PreciseReflectedStreamInvokerProviderBoundarySource = """
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse>
{
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
public interface ICqrsStreamInvokerProvider
{
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor);
}
public interface IEnumeratesCqrsStreamInvokerDescriptors
{
IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors();
}
public sealed class CqrsStreamInvokerDescriptor
{
public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
}
public sealed class CqrsStreamInvokerDescriptorEntry
{
public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor)
{
RequestType = requestType;
ResponseType = responseType;
Descriptor = descriptor;
}
public Type RequestType { get; }
public Type ResponseType { get; }
public CqrsStreamInvokerDescriptor Descriptor { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed class Container
{
private sealed record HiddenResponse();
private sealed record HiddenStream() : IStreamRequest<HiddenResponse[]>;
private sealed class HiddenHandler : IStreamRequestHandler<HiddenStream, HiddenResponse[]>
{
public async IAsyncEnumerable<HiddenResponse[]> Handle(HiddenStream request, CancellationToken cancellationToken)
{
yield return Array.Empty<HiddenResponse>();
await Task.CompletedTask;
}
}
}
}
""";
/// <summary> /// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary> /// </summary>
@ -2911,322 +2383,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.That( Assert.That(
generatedSource, generatedSource,
Does.Contain( Does.Contain(
"global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()")); "public global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()"));
});
}
/// <summary>
/// 验证当 handler 实现类型隐藏、但 request handler interface 仍可见时,
/// 生成器仍会发射 request invoker provider 元数据,而不是因为实现类型不可直接引用而整体退回反射路径。
/// </summary>
[Test]
public void Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface()
{
var generatedSource = RunGenerator(HiddenImplementationRequestInvokerProviderSource);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsRequestInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.VisibleRequest), typeof(string),"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<global::TestApp.VisibleRequest, string>), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)"));
Assert.That(
generatedSource,
Does.Contain(
"var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<global::TestApp.VisibleRequest, string>)handler;"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>ICqrsRequestInvokerProvider</c> 时,
/// 生成器会整体跳过 request invoker provider 元数据发射,而不是输出半套 descriptor 成员。
/// </summary>
[Test]
public void Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface()
{
var generatedSource = RunGenerator(RemoveBlock(
RequestInvokerProviderSource,
"public interface ICqrsRequestInvokerProvider",
"public interface IEnumeratesCqrsRequestInvokerDescriptors"));
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsRequestInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsRequestInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsRequestInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeRequestHandler0"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>IEnumeratesCqrsRequestInvokerDescriptors</c> 时,
/// 生成器不会只发射 request provider 的部分成员,而是整体保持不生成 provider 元数据。
/// </summary>
[Test]
public void Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator()
{
var generatedSource = RunGenerator(RemoveBlock(
RequestInvokerProviderSource,
"public interface IEnumeratesCqrsRequestInvokerDescriptors",
"public sealed class CqrsRequestInvokerDescriptor"));
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsRequestInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsRequestInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsRequestInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeRequestHandler0"));
});
}
/// <summary>
/// 验证当 request handler 仍需走 precise reflected 注册时,
/// 生成器即使检测到 request invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。
/// </summary>
[Test]
public void Does_Not_Emit_Request_Invoker_Provider_Metadata_For_Precise_Reflected_Request_Registrations()
{
var generatedSource = RunGenerator(PreciseReflectedRequestInvokerProviderBoundarySource);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType("));
Assert.That(generatedSource, Does.Contain(".MakeArrayType()"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsRequestInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsRequestInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsRequestInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeRequestHandler0"));
});
}
/// <summary>
/// 验证当 runtime 暴露 stream invoker provider 契约时,生成器会让 generated registry 同时发射
/// stream invoker 描述符与对应的开放静态 invoker 方法。
/// </summary>
[Test]
public void Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available()
{
var execution = ExecuteGenerator(StreamInvokerProviderSource);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors, Is.Empty);
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsStreamInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.VisibleStream), typeof(int),"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.VisibleStream, int>), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)"));
Assert.That(
generatedSource,
Does.Contain(
"public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)"));
Assert.That(
generatedSource,
Does.Contain(
"private static object InvokeStreamHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)"));
Assert.That(
generatedSource,
Does.Contain(
"var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.VisibleStream, int>)handler;"));
Assert.That(
generatedSource,
Does.Contain("return typedHandler.Handle(typedRequest, cancellationToken);"));
Assert.That(
generatedSource,
Does.Contain(
"global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors.GetDescriptors()"));
});
}
/// <summary>
/// 验证当 handler 实现类型隐藏、但 stream handler interface 仍可见时,
/// 生成器仍会发射 stream invoker provider 元数据,而不是放弃生成稳定的 generated invoker 桥接。
/// </summary>
[Test]
public void Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface()
{
var generatedSource = RunGenerator(HiddenImplementationStreamInvokerProviderSource);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsStreamInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.VisibleStream), typeof(int),"));
Assert.That(
generatedSource,
Does.Contain(
"new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.VisibleStream, int>), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)"));
Assert.That(
generatedSource,
Does.Contain(
"var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.VisibleStream, int>)handler;"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>ICqrsStreamInvokerProvider</c> 时,
/// 生成器会整体跳过 stream invoker provider 元数据发射,而不是保留孤立的 descriptor 成员。
/// </summary>
[Test]
public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface()
{
var generatedSource = RunGenerator(RemoveBlock(
StreamInvokerProviderSource,
"public interface ICqrsStreamInvokerProvider",
"public interface IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>IEnumeratesCqrsStreamInvokerDescriptors</c> 时,
/// 生成器不会只发射 stream provider 的局部成员,而是整体保持不生成 provider 元数据。
/// </summary>
[Test]
public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator()
{
var generatedSource = RunGenerator(RemoveBlock(
StreamInvokerProviderSource,
"public interface IEnumeratesCqrsStreamInvokerDescriptors",
"public sealed class CqrsStreamInvokerDescriptor"));
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>CqrsStreamInvokerDescriptor</c> 时,
/// 生成器不会继续发射依赖描述符类型的 stream provider 元数据。
/// </summary>
[Test]
public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type()
{
var source = RenameTypeIdentifier(
StreamInvokerProviderSource,
"CqrsStreamInvokerDescriptor",
"MissingCqrsStreamInvokerDescriptor");
var generatedSource = RunGenerator(source);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>CqrsStreamInvokerDescriptorEntry</c> 时,
/// 生成器不会继续保留 stream provider 的枚举接口或静态 invoker 元数据。
/// </summary>
[Test]
public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type()
{
var source = RenameTypeIdentifier(
StreamInvokerProviderSource,
"CqrsStreamInvokerDescriptorEntry",
"MissingCqrsStreamInvokerDescriptorEntry");
var generatedSource = RunGenerator(source);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0"));
});
}
/// <summary>
/// 验证当 stream handler 仍需走 precise reflected 注册时,
/// 生成器即使检测到 stream invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。
/// </summary>
[Test]
public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_For_Precise_Reflected_Stream_Registrations()
{
var generatedSource = RunGenerator(PreciseReflectedStreamInvokerProviderBoundarySource);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<,>).MakeGenericType("));
Assert.That(generatedSource, Does.Contain(".MakeArrayType()"));
Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider"));
Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors"));
Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry("));
Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0"));
}); });
} }
@ -3281,95 +2438,6 @@ public class CqrsHandlerRegistryGeneratorTests
return execution.GeneratedSources[0].content; return execution.GeneratedSources[0].content;
} }
/// <summary>
/// 从测试输入源码中移除两个稳定标记之间的整段合同定义,
/// 避免回归用例依赖三引号字符串中的精确缩进。
/// </summary>
/// <param name="source">原始测试源码。</param>
/// <param name="startMarker">待移除代码块的起始标记。</param>
/// <param name="endMarker">待移除代码块之后紧邻的下一个稳定标记。</param>
/// <returns>移除指定代码块后的新源码。</returns>
private static string RemoveBlock(string source, string startMarker, string endMarker)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(startMarker);
ArgumentNullException.ThrowIfNull(endMarker);
var startIndex = source.IndexOf(startMarker, StringComparison.Ordinal);
if (startIndex < 0)
{
throw new InvalidOperationException("The requested start marker was not found in the generator test input.");
}
var endIndex = source.IndexOf(endMarker, startIndex, StringComparison.Ordinal);
if (endIndex < 0)
{
throw new InvalidOperationException("The requested end marker was not found in the generator test input.");
}
return source.Remove(startIndex, endIndex - startIndex);
}
/// <summary>
/// 仅按完整类型标识符重命名测试输入中的合同类型,避免误伤共享前缀的其他类型名。
/// </summary>
/// <param name="source">原始测试源码。</param>
/// <param name="originalTypeName">原始合同类型名。</param>
/// <param name="replacementTypeName">替换后的占位类型名。</param>
/// <returns>完成精确类型重命名后的源码。</returns>
private static string RenameTypeIdentifier(string source, string originalTypeName, string replacementTypeName)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(originalTypeName);
ArgumentNullException.ThrowIfNull(replacementTypeName);
var result = new System.Text.StringBuilder(source.Length);
var currentIndex = 0;
while (currentIndex < source.Length)
{
var matchIndex = source.IndexOf(originalTypeName, currentIndex, StringComparison.Ordinal);
if (matchIndex < 0)
{
result.Append(source, currentIndex, source.Length - currentIndex);
break;
}
result.Append(source, currentIndex, matchIndex - currentIndex);
if (IsIdentifierBoundary(source, matchIndex - 1) &&
IsIdentifierBoundary(source, matchIndex + originalTypeName.Length))
{
result.Append(replacementTypeName);
}
else
{
result.Append(originalTypeName);
}
currentIndex = matchIndex + originalTypeName.Length;
}
return result.ToString();
}
/// <summary>
/// 判断给定位置是否位于 C# 标识符边界,用于避免把共享前缀的其他类型名一并改写。
/// </summary>
/// <param name="source">待检查的完整源码。</param>
/// <param name="index">边界位置;允许落在字符串两端之外。</param>
/// <returns>若当前位置不在标识符内部,则返回 <see langword="true" />。</returns>
private static bool IsIdentifierBoundary(string source, int index)
{
if (index < 0 || index >= source.Length)
{
return true;
}
var character = source[index];
return !char.IsLetterOrDigit(character) && character != '_';
}
/// <summary> /// <summary>
/// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。 /// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。
/// </summary> /// </summary>

View File

@ -1,111 +0,0 @@
# CQRS 重写迁移验证归档RP-063 至 RP-074
## 说明
- 本文件承接 `cqrs-rewrite-validation-history-through-rp062.md` 之后的详细验证历史。
- active tracking 只保留当前权威验证批次、最近 PR 锚点与下一恢复点;更早的命令级明细统一归档到这里。
## 验证记录
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:确认当时当前分支对应 `PR #305`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable``_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsArchitectureContextIntegrationTests.Handler_Can_Access_Architecture_Context|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Transient_Error_Request_Should_Succeed_Without_Simulated_Errors"`
- 结果:通过
- 备注:`5/5` passed覆盖 generated invoker provider、真实上下文注入与两条重命名高级行为测试
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently"`
- 结果:通过
- 备注:`3/3` passed确认并发首次解析测试在失败路径释放调整后保持通过
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available|FullyQualifiedName~Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes"`
- 结果:通过
- 备注:`3/3` passed确认 provider 生成分支注释与断言顺序修正未改变生成语义
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:构建成功;并行验证期间出现过 `MSB3026` 拷贝重试噪音,属于同时运行多个 `dotnet` 命令时的输出文件竞争,不是持久性编译 warning
- `bash scripts/validate-csharp-naming.sh`
- 结果:通过
- 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归
- `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 test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`4/4` passed确认 hidden implementation + visible interface 场景也会继续发射 request / stream invoker provider 元数据
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"`
- 结果:通过
- 备注:`8/8` passed补齐 hidden implementation + visible interface 场景后,确认 generated request / stream invoker 在 runtime 侧也会优先命中 provider descriptor
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认本轮 precise reflected invoker provider 合同回归未引入 generator 编译告警
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;并行验证时曾出现过 `MSB3026` 输出文件竞争噪音,随后已串行重跑并得到干净构建结果
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_For_Precise_Reflected_Request_Registrations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_For_Precise_Reflected_Stream_Registrations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_For_Hidden_Implementation_With_Visible_Handler_Interface"`
- 结果:通过
- 备注:`4/4` passed串行确认 visible-interface hidden-implementation 仍发射 provider 元数据,而 precise reflected 注册继续保持“不发射 provider descriptor”的当前合同
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release`
- 结果:通过
- 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;无真实编译错误,后续以串行 test 结果作为本轮 authoritative 行为验证
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`6/6` passed锁定 request / stream provider gate 依赖“provider 接口 + descriptor 枚举接口”同时存在,且原有 happy-path 发射仍保持通过
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;当前已确认没有新增 analyzer warning`GFramework.Cqrs.Tests` 仍能完成 Release 构建
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered"`
- 结果:通过
- 备注:`6/6` passed确认 request / stream 的非法 generated invoker 现统一抛出 `InvalidOperationException`,且原有 happy-path 未回归
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认新增 non-enumerating provider 回归未引入构建告警
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"`
- 结果:通过
- 备注:`14/14` passed确认 request / stream 的 generated happy-path、异常路径与 non-enumerating provider 反射回退语义均保持通过
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"`
- 结果:通过
- 备注:`5/5` 通过;覆盖自定义 publisher 顺序、上下文注入、零处理器、首错即停与默认接线复用
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过
- 备注:`41/41` 通过;确认 CQRS 基础设施默认接线与容器行为未回归
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过
- 备注:`42/42` 通过;本轮新增 legacy alias 回填回归后,确认正式 seam 与旧命名空间 alias 仍指向同一实例
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
- 结果:通过
- 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"`
- 结果:通过
- 备注:`22/22` 通过;新增 `PublishAsync` / `CreateStream` 并发首次访问只解析一次 `ICqrsRuntime` 的回归

View File

@ -1,34 +0,0 @@
# CQRS 重写迁移追踪归档RP-062 至 RP-076
## 说明
- 本文件承接从 active trace 中迁出的 `RP-062``RP-076` 阶段细节。
- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`,不要从本归档直接挑选旧阶段作为当前恢复点。
## 覆盖范围
- `CQRS-REWRITE-RP-062``CQRS-REWRITE-RP-076`
- 对应 active trace 清理前的 `2026-04-29``2026-04-30` 阶段记录
## 归档摘要
- `RP-062``PR #305` review follow-up 收敛补齐并发测试、logger provider 恢复、真实上下文注入与 tracking/trace 细节修正
- `RP-063``CQRS vs Mediator` 结构化评估归档
- `RP-064`notification publisher seam 最小实现与回归补齐
- `RP-065``Mediator` 历史测试命名与目录收口
- `RP-066`legacy `ICqrsRuntime` alias compatibility slice 收敛
- `RP-067`generated request invoker provider 最小落地
- `RP-068`generated stream invoker provider 最小落地
- `RP-069`generated invoker 在 hidden-implementation + visible-interface 场景下的发射范围补强
- `RP-070`hidden-implementation generated invoker runtime 回归补强
- `RP-071`precise reflected invoker provider 合同边界回归
- `RP-072`request / stream provider gate 合同回归
- `RP-073`generated invoker provider runtime 失败边界修复
- `RP-074`non-enumerating provider reflection fallback 回归
- `RP-075``PR #307` review follow-up 收敛,补齐 descriptor 合同防御、空枚举回退与文档口径
- `RP-076`stream invoker gate 四项 runtime 合同分支补强,并最终将 active tracking / trace 收敛为单一恢复入口
## 备注
- `RP-063``RP-074` 的详细命令级验证仍以 `archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md` 为准。
- `RP-075``RP-076` 的权威验证结论已同步沉淀到 active tracking / trace后续若需追溯阶段细节应同时参考对应测试文件、提交记录与本归档摘要。

View File

@ -7,54 +7,267 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-076` - 恢复点编号:`CQRS-REWRITE-RP-067`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #307` - 当前焦点:
- 当前结论: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序” - 当前评估结论已明确:`GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,但仓库内部旧总线 API、
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,当前 `RP-076` 已补齐 stream invoker provider gate 的四项 runtime 合同分支 兼容 seam、fallback 旧语义与测试命名仍未完全收口
- `ai-plan` active 入口现以 `PR #307``RP-076` 为唯一权威恢复锚点;更早 PR 与阶段细节均以下方归档为准 - 当前评估结论已明确:相对 `ai-libs/Mediator`框架已吸收统一消息模型、generator 优先注册与热路径缓存思路,
但仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成
- 下一阶段建议优先级已收敛为:`notification publisher seam``dispatch/invoker 生成前移``pipeline 分层扩展`
`可观测性 seam``benchmark / allocation baseline`
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
- 已完成一轮 notification publisher seam 最小落地:`GFramework.Cqrs` 新增 `INotificationPublisher`
`NotificationPublishContext<TNotification>` 与默认 `SequentialNotificationPublisher`
- `CqrsDispatcher` 现会在解析当前通知处理器集合后,把执行顺序委托给 publisher seam默认行为仍保持
“零处理器静默完成、顺序执行、首错即停”
- `CqrsRuntimeFactory``CqrsRuntimeModule``GFramework.Tests.Common.CqrsTestRuntime` 现支持在 runtime 创建前复用
容器里已显式注册的 `INotificationPublisher`
- 已补充 `CqrsNotificationPublisherTests`,覆盖自定义 publisher 接管、上下文注入、零处理器静默完成、首错即停,以及
`RegisterInfrastructure` 默认接线复用预注册 publisher 的回归
- 已完成一轮 `Mediator` 测试命名收口:
- `MediatorAdvancedFeaturesTests` -> `CqrsArchitectureContextAdvancedFeaturesTests`
- `MediatorArchitectureIntegrationTests` -> `CqrsArchitectureContextIntegrationTests`
- `MediatorComprehensiveTests` -> `ArchitectureContextComprehensiveTests`
- `GFramework.Cqrs.Tests` 中这三份历史测试现已统一迁入 `Cqrs/` 目录,并将命名空间、类名、中文注释与嵌套测试类型中的
`Mediator` 语义收口为 `CQRS` / `ArchitectureContext`
- 已补充 `ArchitectureContextTests` 并发 lazy-resolution 回归,锁定 `PublishAsync(...)``CreateStream(...)`
在并发首次访问时也只会解析一次 `ICqrsRuntime`
- 已完成一轮 `LegacyICqrsRuntime` compatibility slice 收口:
- `CqrsRuntimeModule``GFramework.Tests.Common.CqrsTestRuntime` 现把 legacy alias 注册收敛到显式 helper
- `MicrosoftDiContainerTests` 已补充“只预注册正式 `ICqrsRuntime` seam 时,也会回填 legacy alias 且保持同实例”的回归
- `GFramework.Core.Abstractions/README.md``docs/zh-CN/abstractions/core-abstractions.md`
`docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留,
新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`
- 已完成一轮 `dispatch/invoker` 生成前移的最小 request 切片:
- `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider``IEnumeratesCqrsRequestInvokerDescriptors`
`CqrsRequestInvokerDescriptor``CqrsRequestInvokerDescriptorEntry`
- generated registry 若实现 request invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器,
并把 provider 枚举出的 request invoker 描述符写入 dispatcher 的进程级弱缓存
- `CqrsDispatcher` 现会在首次创建 request dispatch binding 时优先命中 generated request invoker 描述符;
未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径
- `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义
- `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时generated registry 会额外发射 request invoker provider 成员与 invoker 方法
- 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
- 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers
- 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor而不是每次 `SendAsync` 都重建整条 `next` 委托链
- 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序
- 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext`
- 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext`
- 已补充非 `IArchitectureContext` 的 dispatcher 失败语义回归,锁定 context-aware request / notification / stream handler 在注入前置条件不满足时会显式抛出异常
- 已补充 registrar fallback 失败分支回归,锁定 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
- 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找
- 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher``CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配
- 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义
- 已完成 `PR #304` review follow-up 收敛:`CqrsDispatcher` 现补齐 pipeline executor / continuation 缓存的线程模型文档,并把 request pipeline invoker 从按 `behaviorCount` 重复创建收敛为 binding 内复用
- 已补齐 `CqrsDispatcherContextValidationTests` 三个上下文校验 handler 的 XML `param` / `returns` 注释,以及 `DispatcherNotificationContextRefreshNotification``DispatcherStreamContextRefreshRequest``DispatchId` XML 参数注释,收敛上一轮 PR review 遗留的文档类 minor feedback
- 已收紧 CQRS / generator 回归测试的脆弱断言日志断言改为语义匹配precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义
- 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
## 当前状态摘要
- 已完成 `Mediator` 外部依赖移除、CQRS runtime 重建、默认架构接线和显式程序集 handler 注册入口
- 已完成 `GFramework.Cqrs.Abstractions` / `GFramework.Cqrs` 项目骨架与 runtime seam 收敛
- 已完成 handler registry generator 的多轮收敛,当前合法 closed handler contract 已统一收敛到更窄的注册路径
- 已完成一轮公开入口文档与 source-generator 命名空间收口
- 已完成一轮 `CQRS vs Mediator` 对照评估,确认当前主问题已从“是否能替代外部依赖”转为“框架内部收口与能力深化顺序”
- 已接入 `$gframework-pr-review`,可直接抓取当前分支对应 PR 的 CodeRabbit 评论、checks 和测试结果
## 当前活跃事实 ## 当前活跃事实
- 当前分支对应 `PR #307`,状态为 `OPEN` - `Phase 8` 仍是当前主线,不再回退到 `Phase 7`
- latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛 - `2026-04-20` 已重新执行 `$gframework-pr-review`
- 远端 `CTRF` 最新汇总为 `2247/2247` passed - 当前分支对应 `PR #261`,状态为 `OPEN`
- `MegaLinter` 当前只暴露 `dotnet-format``Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 - latest reviewed commit 当前剩余 `1` 条 open CodeRabbit thread指向 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md``RP-047``RP-050` 的历史语义冲突
- 本地已同步修正该追踪歧义:`RP-047` 明确标注为已被 `RP-050` 覆盖,后续不得恢复 `MakePointerType()` precise registration
- 远端测试信号保持通过:最新 CTRF 汇总为 `2118/2118` passedMegaLinter 仅剩 `dotnet-format` restore failure 预警,当前未提供本地仍然成立的文件级格式问题
- `2026-04-20` 已完成一轮冷启动反射收敛:
- generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke`
- 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径
- `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖
- `2026-04-20` 已完成一轮 generator 覆盖面扩展:
- `CqrsHandlerRegistryGenerator` 现会在 runtime type 建模入口直接拒绝 `IPointerTypeSymbol``IFunctionPointerTypeSymbol`
- `CanReferenceFromGeneratedRegistry` 不再递归判断 pointer / function pointer 的内部元素,而是统一返回 `false`
- 相关 source-generator 回归已改为区分输入源诊断与生成源诊断,避免把非法泛型合同误判为成功生成
- `2026-04-20` 已完成一轮 registrar reflection 路径收敛:
- `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表
- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选
- `GFramework.Cqrs.Tests` 已补充 registrar 静态缓存隔离与 supported interface 缓存复用回归
- `2026-04-20` 已完成一轮 registrar 去重路径收敛:
- `CqrsHandlerRegistrar` 现会在单次 reflection 注册流程开始时构建已注册 handler 映射索引
- 同一批注册中后续 duplicate handler mapping 不再重复线性扫描 `IServiceCollection`
- `GFramework.Cqrs.Tests` 已补充“程序集返回重复 handler 类型时仍只注册一份映射”的回归
- `2026-04-29` 已完成一轮 generator fallback 元数据收敛:
- `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否同时支持 `params string[]``params Type[]` 两类 `CqrsReflectionFallbackAttribute` 构造函数
- 当本轮 fallback handlers 全部可被生成代码直接引用时,生成器会优先发射 `typeof(...)` 形式的程序集级 fallback 元数据,减少运行时 `Assembly.GetType(...)` 回查
- 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续统一发射字符串元数据,避免 mixed 场景只恢复部分 handlers
- `GFramework.SourceGenerators.Tests` 已补充 runtime 同时暴露两类构造函数时优先选择直接 `Type` 元数据的回归
- `2026-04-29` 已完成一轮 mixed fallback 元数据拆分:
- `CqrsReflectionFallbackAttribute` 现显式允许 `AllowMultiple = true`
- `CqrsHandlerRegistryGenerator` 现会探测 runtime 是否允许多个 fallback 特性实例
- 当本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers且 runtime 同时支持 `Type[]``string[]` 和多实例特性时,生成器会拆分输出两段 fallback 元数据
- `GFramework.Cqrs.Tests` 已补充 mixed fallback metadata 回归,锁定 registrar 只对字符串条目执行定向 `Assembly.GetType(...)`
- `GFramework.SourceGenerators.Tests` 已补充 mixed fallback emission 回归,锁定 generator 会输出两个程序集级 fallback 特性实例而不是整体退回字符串
- `2026-04-29` 已重新执行 `$gframework-pr-review`
- 当前分支对应 `PR #302`,状态为 `OPEN`
- latest reviewed commit 当前剩余 `3` 条 open AI review threads`2` 条 Greptile、`1` 条 CodeRabbit
- 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断
- 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档
- `2026-04-30` 已重新执行 `$gframework-pr-review`
- 当前分支对应 `PR #305`,状态为 `OPEN`
- 当前抓取到 `9` 条 CodeRabbit open threads、`2` 条 Greptile open threads远端 CTRF 汇总为 `2214/2214` passedMegaLinter 仍只暴露 `dotnet-format``Restore operation failed` 环境噪音
- 本地核对后,已确认以下评论仍然成立并已完成修正:`ArchitectureContextTests` 并发测试失败路径释放、`CqrsGeneratedRequestInvokerProviderTests` 的全局 logger provider 恢复与私有缓存断言解耦、`CqrsArchitectureContextIntegrationTests` 的真实上下文注入断言、`GeneratedRequestInvokerRequest` / `INotificationPublisher` XML 文档、`CqrsHandlerRegistrar` 的 provider 注册顺序、`CqrsTestRuntime` 的 legacy alias 显式失败模式,以及 `cqrs-rewrite` trace 重复标题
- 对于 `ICqrsRequestInvokerProvider` / generated `TryGetDescriptor(...)` 相关 Greptile 评论,本地评估后未改 dispatcher 热路径语义;改为补齐公开注释与生成器方法级注释,明确默认 runtime 只在注册阶段经 `IEnumeratesCqrsRequestInvokerDescriptors` 预热缓存,`TryGetDescriptor(...)` 保留为显式查询 seam
- 本轮额外修正了 `GFramework.SourceGenerators.Tests` 中先读取 `GeneratedSources[0]` 再断言长度的脆弱顺序,并将 `ArchitectureContextTests` 的并发 orchestration 收敛到公共 helper消除本轮引入的 `MA0051` warning
- `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强:
- `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归
- 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()`
- 本轮定向测试未暴露数组发射缺陷,因此未改动 fallback 合同选择逻辑,也未调整 direct / named / mixed fallback 排版路径
- `2026-04-29` 已补齐一轮外部程序集隐藏泛型定义回归覆盖:
- `GFramework.SourceGenerators.Tests` 已新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归
- 当前生成器会继续为这类 handler 合同发射 `ResolveReferencedAssemblyType(...) + MakeGenericType(...)` 组合,而不是退回字符串 fallback 元数据
- 本轮定向测试未暴露新的实现缺口,因此未改动 direct / named / mixed fallback 选择逻辑,也未调整 generator runtime type 建模实现
- `2026-04-29` 已完成一轮缓存工厂闭包收敛:
- `CqrsDispatcher` 现会在 notification / stream / request binding 与 pipeline executor 缓存入口优先使用无捕获工厂
- `CqrsHandlerRegistrar` 现会在程序集元数据缓存与可加载类型缓存入口复用 `static` 工厂 + 显式状态参数
- 本轮未改动公开语义,也未修改 fallback 合同与 handler / behavior 生命周期边界
- `2026-04-29` 已完成一轮 request pipeline executor 形状缓存:
- `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor
- 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义
- `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归
- `2026-04-29` 已完成一轮 cached executor 上下文刷新回归补强:
- `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext`
- `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文
- 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`
- `2026-04-29` 已完成一轮 cached notification / stream binding 上下文刷新回归补强:
- `GFramework.Cqrs.Tests` 已新增 `DispatcherNotificationContextRefresh*``DispatcherStreamContextRefresh*` 测试替身,分别记录 notification handler 与 stream handler 在重复分发时观察到的实例身份与 `ArchitectureContext`
- `CqrsDispatcherCacheTests` 现明确断言:同一个 cached notification / stream dispatch binding 在重复分发时会继续命中同一 binding但不会跨分发保留旧上下文
- 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`
- `2026-04-29` 已完成一轮 dispatcher 上下文前置条件失败语义回归:
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已通过公开工厂 `CqrsRuntimeFactory.CreateRuntime(...)` 锁定默认 dispatcher 的失败语义
- 当 context-aware request / notification / stream handler 遇到仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文时dispatcher 会在调用前显式抛出 `InvalidOperationException`
- 本轮只补测试,不改 runtime 实现与文档口径
- `2026-04-29` 已接受一轮 delegated registrar fallback 失败分支测试:
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义
- 主线程已复核该新文件并重新执行定向测试,确认当前 registrar 在 fallback 元数据失效时仍保持“跳过条目 + 记录告警”的既有语义
- `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试:
- `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义
- 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖
- `2026-04-29` 已完成一轮 CQRS 入口文档对齐:
- `GFramework.Cqrs/README.md``docs/zh-CN/core/cqrs.md``docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义
- `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理:
- `CqrsHandlerRegistryGenerator` 的运行时类型引用模型已移除不可达的 pointer 子结构
- `SourceEmission` 不再保留 `MakePointerType()` 源码发射分支,`RuntimeTypeReferences` 也已删掉对应的外部程序集递归扫描死代码
- pointer / function pointer 的拒绝语义保持不变direct / named / mixed fallback 逻辑未改动
- 当前工作区相对 `origin/main` 的累计 diff 已达到 `14 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition
- `2026-04-30` 已完成一轮 `CQRS vs Mediator` 结构化评估:
- 生产依赖与默认 runtime 接线层面,`GFramework.Cqrs` 已完成对外部 `Mediator` 的替代
- 仓库内部收口层面,旧 `Command` / `Query` API、`LegacyICqrsRuntime` 别名、fallback 空 marker 兼容语义与
`Mediator` 测试命名仍然存在
- 设计吸收层面当前已吸收统一消息模型、generator 优先注册与反射收敛思路;仍未完整吸收 publisher 策略抽象、
stream / exception pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成
- 详细结论与证据已归档到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
- `2026-04-30` 已接受两条只读 subagent 结论并完成 notification publisher seam 最小实现:
- 相对 `ai-libs/Mediator`,本轮只吸收 notification publisher 的策略接缝,不照搬 `NotificationHandlers<T>` 包装、
并行 publisher 或异常聚合语义
- 当前 seam 刻意保持在默认 runtime 内部:`ICqrsRuntime.PublishAsync(...)` 外形不变dispatcher 仍负责 handler 解析与
`IContextAware` 上下文注入
- 用户若需替换通知发布策略,只需在 runtime 创建前向容器显式注册 `INotificationPublisher`
- `2026-04-30` 已接受三条 worker 切片并完成一轮测试命名收口:
- 三个 worker 分别独立拥有一份 `GFramework.Cqrs.Tests/Mediator/*.cs` 文件,主线程只做集成验证与后续追踪更新
- 当前分支已不再保留 `GFramework.Cqrs.Tests/Mediator/` 目录下的生产内涵测试,相关文件均迁移到 `GFramework.Cqrs.Tests/Cqrs/`
- 本轮没有修改测试行为,只收口命名、注释、局部变量与嵌套测试类型语义
- 当前主线优先级:
- dispatch/invoker 反射占比继续下降,并优先评估生成前移方案
- 基于已落地 publisher seam继续评估是否需要公开配置面、并行策略或 telemetry decorator
- package / facade / 兼容层继续收口
- pipeline 分层扩展、可观测性 seam 与 benchmark baseline 进入中期候选
## 当前风险 ## 当前风险
- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证 - 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” - 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 - 当前对外替代已基本完成,但若不单独规划旧 `Command` / `Query``LegacyICqrsRuntime` 与测试命名的收口顺序,
后续仍会持续混淆“生产替代已完成”与“仓库内部收口未完成”这两个不同结论
## 最近权威验证
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #307`,本轮剩余 open AI feedback 主要集中在 `ai-plan` 收敛
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过,`5/5` passed
## 下一推荐步骤
1. 继续处理 `PR #307` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致
2. 若继续推进代码切片,优先复核 request 侧是否存在与 stream gate 对称的生成合同遗漏,再决定是否补同批 generator 回归
3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build 或 targeted test 作为权威验证
## 活跃文档 ## 活跃文档
- 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md) - 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md)
- 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md) - 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md)
- `RP-063``RP-074` 验证归档:[cqrs-rewrite-validation-history-rp063-through-rp074.md](../archive/todos/cqrs-rewrite-validation-history-rp063-through-rp074.md)
- `RP-062``RP-076` trace 归档:[cqrs-rewrite-history-rp062-through-rp076.md](../archive/traces/cqrs-rewrite-history-rp062-through-rp076.md)
- CQRS 与 Mediator 评估归档:[cqrs-vs-mediator-assessment-rp063.md](../archive/todos/cqrs-vs-mediator-assessment-rp063.md) - CQRS 与 Mediator 评估归档:[cqrs-vs-mediator-assessment-rp063.md](../archive/todos/cqrs-vs-mediator-assessment-rp063.md)
- 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md) - 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md)
- `RP-046``RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md) - `RP-046``RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md)
## 说明 ## 验证说明
- `PR #261``PR #302``PR #305` 及更早阶段的详细过程已不再作为 active 恢复入口;如需追溯,以对应归档文件为准 - `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档
- active tracking 仅保留当前恢复点、当前风险、最近权威验证与下一推荐步骤,避免 `boot` 落到历史阶段细节 - `RP-046``RP-062` 的历史验证命令与阶段性结果已移入验证归档active tracking 只保留当前恢复入口需要的最新验证
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #305`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable``_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsArchitectureContextIntegrationTests.Handler_Can_Access_Architecture_Context|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Transient_Error_Request_Should_Succeed_Without_Simulated_Errors"`
- 结果:通过
- 备注:`5/5` passed覆盖 generated invoker provider、真实上下文注入与两条重命名高级行为测试
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently"`
- 结果:通过
- 备注:`3/3` passed确认并发首次解析测试在失败路径释放调整后保持通过
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available|FullyQualifiedName~Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes"`
- 结果:通过
- 备注:`3/3` passed确认 provider 生成分支注释与断言顺序修正未改变生成语义
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:构建成功;并行验证期间出现过 `MSB3026` 拷贝重试噪音,属于同时运行多个 `dotnet` 命令时的输出文件竞争,不是持久性编译 warning
- `bash scripts/validate-csharp-naming.sh`
- 结果:通过
- 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;本轮确认 notification publisher seam、README 与文档更新未引入 `GFramework.Cqrs` 构建告警
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"`
- 结果:通过
- 备注:`5/5` 通过;覆盖自定义 publisher 顺序、上下文注入、零处理器、首错即停与默认接线复用
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过
- 备注:`41/41` 通过;确认 CQRS 基础设施默认接线与容器行为未回归
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过
- 备注:`42/42` 通过;本轮新增 legacy alias 回填回归后,确认正式 seam 与旧命名空间 alias 仍指向同一实例
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
- 结果:通过
- 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"`
- 结果:通过
- 备注:`22/22` 通过;新增 `PublishAsync` / `CreateStream` 并发首次访问只解析一次 `ICqrsRuntime` 的回归
## 下一步
1. 基于已落地的 notification publisher seam评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator
2. 基于已落地的 request invoker provider评估是否继续把 notification / stream 的 invoker 也前移,或先补 provider 发现/诊断与文档入口
3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级

View File

@ -2,31 +2,169 @@
## 2026-04-30 ## 2026-04-30
### 阶段:PR #307 active 入口收敛CQRS-REWRITE-RP-076 ### 阶段:generated request invoker provider 最小落地CQRS-REWRITE-RP-067
- 继续沿用 `$gframework-pr-review``PR #307` 做 latest-head triage本轮只处理仍成立的 `ai-plan` 恢复入口问题 - 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
- 主线程确认当前远端权威信号: - 在 `RP-066` 提交后复算 branch diff相对 `origin/main` 增长到 `22 files`,仍明显低于 `50 files` stop condition因此继续下一批
- 当前分支对应 `PR #307`,状态为 `OPEN` - 本轮 critical path 保持在主线程,本地完成 `dispatch/invoker` 生成前移的最小 request 切片;尝试委派 source-generator 测试给 worker 时因 subagent 名额已满失败,因此主线程直接接管该测试修改
- 远端 `CTRF` 最新汇总为 `2247/2247` passed - 本轮关键设计调整:
- `MegaLinter` 仅剩 `dotnet-format``Restore operation failed` 环境噪音 - 不按 `requestType.Assembly` 做 provider 发现,避免“请求定义在 A、handler 与 generated registry 在 B”时漏掉 generated invoker
- 仍未闭环的 review 重点集中在 `cqrs-rewrite` active tracking / trace 仍保留过多历史锚点,而非新的运行时代码缺陷 - generated registry 若实现 `ICqrsRequestInvokerProvider`registrar 会在激活 registry 后把 provider 注册进容器,并通过 `IEnumeratesCqrsRequestInvokerDescriptors` 把描述符写入 dispatcher 的进程级弱缓存
- 本轮决策: - dispatcher 首次创建 request dispatch binding 时只按 `requestType + responseType` 读取静态弱缓存,不依赖具体容器实例;未命中时仍走既有反射创建路径
- 将 active tracking 收敛为单一恢复入口,只保留 `RP-076``PR #307`、活跃风险、最近权威验证与下一推荐步骤 - 已完成实现:
- 将 active trace 收敛为当前阶段的关键事实与决策,不再在默认恢复入口中保留 `RP-062` 之后的长阶段流水账 - `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider``IEnumeratesCqrsRequestInvokerDescriptors`
- 新增 `archive/traces/cqrs-rewrite-history-rp062-through-rp076.md` 承接 `RP-062``RP-076` 的详细 trace 历史,保持旧阶段仍可追溯 `CqrsRequestInvokerDescriptor``CqrsRequestInvokerDescriptorEntry`
- `CqrsHandlerRegistrar` 现会识别 generated registry 的 request invoker provider 能力,并登记 provider 与 request invoker 描述符
- `CqrsDispatcher` 新增 generated request invoker 弱缓存,并在 request binding 创建时优先消费该元数据
- `CqrsHandlerRegistryGenerator` 在 runtime 合同可用时,会让 generated registry 额外实现 request invoker provider 相关接口,并发射 descriptor 列表、`TryGetDescriptor(...)``GetDescriptors()` 与 request invoker 静态方法
- 已补充测试:
- `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果
- `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法
### 验证RP-076 ### 验证RP-067
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
- 结果:通过 - 结果:通过,`22/22` passed
- 备注:确认 `PR #307` 的当前 review 重点已收敛到 `ai-plan` 文档收尾 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - 结果:通过,`1/1` passed
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
### 当前下一步RP-067
1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义
2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files`
### 阶段LegacyICqrsRuntime compatibility slice 收口CQRS-REWRITE-RP-066
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
- 在 `RP-065` 之后复算 branch diff相对 `origin/main` 仍为 `19 files`,明显低于 `50 files` stop condition因此继续下一批
- 本轮按“关键路径本地、非冲突文档委派”的方式拆成两个切片:
- worker`GFramework.Core.Abstractions/README.md``docs/zh-CN/abstractions/core-abstractions.md``docs/zh-CN/core/cqrs.md`
- 主线程:`GFramework.Core/Services/Modules/CqrsRuntimeModule.cs``GFramework.Tests.Common/CqrsTestRuntime.cs``GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs`
- 接受只读 subagent 结论后,将 `LegacyICqrsRuntime` 定位为“容器兼容层”,明确本轮不删除别名、不改 dispatcher 主体、不与旧 `Command` / `Query` API 清理混做
- 主线程已完成:
- `CqrsRuntimeModule` 把 legacy alias 注册收敛到 `RegisterLegacyRuntimeAlias(...)` helper并在 XML 文档里明确新旧服务类型解析到同一 runtime 实例
- `CqrsTestRuntime.RegisterInfrastructure(...)` 现也通过同名 helper 补齐 legacy alias当容器只预注册正式 `ICqrsRuntime` seam 时,会在幂等接线时回填旧命名空间 alias
- `MicrosoftDiContainerTests` 新增 `RegisterInfrastructure_Should_Backfill_Legacy_Cqrs_Runtime_Alias_With_The_Same_Instance`,锁定“只存在正式 seam 时也会补旧 alias且两者仍指向同一实例”的兼容合同
- worker 已完成文档收口:
- `GFramework.Core.Abstractions/README.md`
- `docs/zh-CN/abstractions/core-abstractions.md`
- `docs/zh-CN/core/cqrs.md`
- 三处文档都已明确:`GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 只是旧命名空间下保留的 compatibility alias新代码应依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`
### 验证RP-066
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过,`42/42` passed
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
### 当前下一步RP-066
1. 在保持 branch diff 低于阈值的前提下,回到 `dispatch/invoker` 生成前移主线
2. 优先尝试只覆盖 request 路径的 generated invoker/provider 最小切片,避免一次卷入 notification / stream / pipeline executor
3. 下一次 batch 结束后继续复算 branch diff确认距 `50 files` stop condition 的剩余 headroom
### 阶段:测试命名收口与 ArchitectureContext lazy-resolution 回归CQRS-REWRITE-RP-065
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
- `22f608eb` 之后复算 branch diff相对 `origin/main` 已达到 `18 files`,仍明显低于 `50 files` stop condition因此继续下一批
- 本轮拆成四个互不冲突切片:
- worker 1`MediatorAdvancedFeaturesTests.cs`
- worker 2`MediatorArchitectureIntegrationTests.cs`
- worker 3`MediatorComprehensiveTests.cs`
- 主线程:`GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs`
- 三个 worker 均只收口单文件命名与注释语义,并把测试文件迁移到 `GFramework.Cqrs.Tests/Cqrs/`
- 主线程新增 `ArchitectureContextTests` 并发 lazy-resolution 回归,锁定:
- `PublishAsync(...)` 在并发首次访问时只解析一次 `ICqrsRuntime`
- `CreateStream(...)` 在并发首次访问时只解析一次 `ICqrsRuntime`
- 集成后已确认三份测试文件中不再残留 `GFramework.Cqrs.Tests.Mediator` 命名空间或 `Mediator` 语义命名
### 验证RP-065
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"`
- 结果:通过,`22/22` passed
### 当前下一步RP-065
1. 继续 `Phase 8` 主线,回到 `dispatch/invoker` 生成前移或 `LegacyICqrsRuntime` 收口的下一个低风险切片
2. 在下一次 batch 结束后复算 branch diff确认距 `50 files` stop condition 的剩余 headroom
### 阶段notification publisher seam 最小落地CQRS-REWRITE-RP-064
- 本轮按 `gframework-batch-boot 50` 继续 `cqrs-rewrite`,基线使用本地现有 `origin/main`
- 当前 branch diff 相对 `origin/main` 开始时仅 `3 files / 164 lines`,远低于 `50 files` stop condition因此继续推进真实代码切片
- 主线程锁定 `notification publisher seam` 为本轮最低风险高收益切片,并保持关键路径在本地实现
- 接受两条只读 subagent 结论:
- 对照 `ai-libs/Mediator` 后,只吸收 notification publisher 策略接缝,不在本轮引入并行 publisher、异常聚合或公开配置面
- 现有仓库测试需要锁定的兼容语义是:零处理器静默完成、顺序执行、首错即停、上下文逐次注入
- 已完成实现:
- `GFramework.Cqrs` 新增 `INotificationPublisher``NotificationPublishContext<TNotification>`
`DelegatingNotificationPublishContext<TNotification, TState>` 与默认 `SequentialNotificationPublisher`
- `CqrsDispatcher.PublishAsync(...)` 改为解析 handlers 后构造发布上下文,并委托给 publisher seam 执行
- `CqrsRuntimeFactory``CqrsRuntimeModule``GFramework.Tests.Common.CqrsTestRuntime` 现会在 runtime 创建前复用容器里已注册的 `INotificationPublisher`
- `GFramework.Cqrs.Tests` 新增 `CqrsNotificationPublisherTests`,覆盖自定义 publisher、上下文注入、零处理器、首错即停与默认接线复用
- `GFramework.Cqrs/README.md``docs/zh-CN/core/cqrs.md` 已同步说明默认通知语义与可替换 seam
- 中途验证曾因并行 .NET 构建产生输出文件锁噪音;已改为串行重跑并获取干净结果
### 验证RP-064
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"`
- 结果:通过,`5/5` passed - 结果:通过,`5/5` passed
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过,`41/41` 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`
- 结果:通过
### 当前下一步RP-076 ### 当前下一步RP-064
1. 继续按 `PR #307` 的 latest-head review 收尾,优先保持 active tracking 与 active trace 的单一锚点一致 1. 评估 notification publisher seam 的第二阶段是否需要公开配置面、并行 publisher 或 telemetry decorator
2. 若继续推进代码切片,先复核 request 侧是否仍存在与 stream invoker gate 对称的生成合同遗漏 2. 把 `dispatch/invoker` 生成前移重新拉回 `Phase 8` 主线,作为下一个实现切片
3. 进入下一批前继续使用最小 Release build 或 targeted test 作为权威验证,避免把环境噪音误判为代码问题
### 阶段CQRS vs Mediator 评估归档CQRS-REWRITE-RP-063
- 本轮按用户要求使用 `gframework-boot` 启动上下文后,先完成 `cqrs-rewrite` 现状核对,再并行对照
`GFramework.Cqrs``ai-libs/Mediator`
- 只读评估结论已归档到 `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md`
- 本轮关键判断:
- `GFramework.Cqrs` 已完成对外部 `Mediator` 作为生产 runtime 依赖的替代
- 当前尚未完成的是仓库内部旧 `Command` / `Query` API、兼容 seam、fallback 旧语义与测试命名的收口
- 当前已吸收 `Mediator` 的统一消息模型、generator 优先注册与热路径缓存思路
- 当前仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成
- 本轮把默认下一步从“继续盯 PR thread”调整为“围绕 publisher seam 与 dispatch/invoker 生成前移做下一轮设计收敛”
### 验证RP-063
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
## 活跃事实
- 当前主题仍处于 `Phase 8`
- 当前主题的主问题已从“是否完成外部依赖替代”转为“内部兼容层收口顺序与下一轮能力深化优先级”
- 已完成阶段的详细执行历史不再留在 active trace默认恢复入口只保留当前恢复点、活跃事实、风险与下一步
## 当前风险
- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响
- 若不把“生产替代完成”与“仓库内部收口完成”分开记录,后续很容易重复争论当前 CQRS 迁移是否已经完成
## Archive Context
- 当前评估归档:
- `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md`
- 历史 trace 归档:
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-through-rp043.md`
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md`
## 当前下一步
1. 补一轮最小 Release 构建验证,确认本次 `ai-plan` 与评估文档更新未引入仓库级异常
2. 以 `notification publisher seam``dispatch/invoker` 生成前移为优先对象,形成下一轮可执行设计

View File

@ -9,14 +9,14 @@
## 2026-04-29 / RP-049 ## 2026-04-29 / RP-049
- 重新进入时确认当前分支仍为 `docs/sdk-update-documentation`,但上游恢复路径不再依赖旧 PR 线程,而是改用 `origin/main` 作为 batch stop-condition baseline。 - 重新进入时确认当前分支仍为 `docs/sdk-update-documentation`,但上游恢复路径不再依赖旧 PR 线程,而是改用 `origin/main` 作为 batch stop-condition baseline。
- 本轮工作树在 reader-facing 文案收口后相对 `origin/main` 来到 `13` files / `132` lines仍明显低于 `$gframework-batch-boot 50` 的阈值。 - 本轮工作树在 reader-facing 文案收口后相对 `origin/main` 来到 `13` files / `132` lines`,仍明显低于 `$gframework-batch-boot 50` 的阈值。
- 主线程接受的变更范围限定在 `docs/zh-CN/godot/*``game/*` 和少量 README 标签,不扩展到导航重写或大型结构改稿。 - 主线程接受的变更范围限定在 `docs/zh-CN/godot/*``game/*` 和少量 README 标签,不扩展到导航重写或大型结构改稿。
## 2026-04-29 / RP-050 ## 2026-04-29 / RP-050
- 在 `RP-049` 的基础上继续做第 2 批低风险 reader-facing 收口,触达 `game/data.md``game/storage.md``godot/ui.md``GFramework.Cqrs.Abstractions/README.md``GFramework.SourceGenerators.Common/README.md` - 在 `RP-049` 的基础上继续做第 2 批低风险 reader-facing 收口,触达 `game/data.md``game/storage.md``godot/ui.md``GFramework.Cqrs.Abstractions/README.md``GFramework.SourceGenerators.Common/README.md`
- 决策上只接受“改句子即可闭环”的问题,不把 README 子系统地图或结构级重写混入同一轮。 - 决策上只接受“改句子即可闭环”的问题,不把 README 子系统地图或结构级重写混入同一轮。
- 当轮工作树相对 `origin/main``18` files / `225` lines仍保留充足余量。 - 当轮工作树相对 `origin/main``18` files / `225` lines`,仍保留充足余量。
## 2026-04-29 / RP-051 ## 2026-04-29 / RP-051
@ -27,5 +27,5 @@
## 2026-04-30 / RP-052 ## 2026-04-30 / RP-052
- 该批次提交为 `f88f96c3``docs(source-generators): 补充生成器专题覆盖并更新进度`)。 - 该批次提交为 `f88f96c3``docs(source-generators): 补充生成器专题覆盖并更新进度`)。
- 提交后重新计算确认 committed branch diff vs `origin/main` 已回落到 `8` files / `337` lines说明提交前的 `39` files / `2555` lines 只是临时工作树峰值,不应继续作为默认恢复指标。 - 提交后重新计算确认 committed branch diff vs `origin/main` 已回落到 `8` files / `337` lines`,说明提交前的 `39` files / `2555` lines` 只是临时工作树峰值,不应继续作为默认恢复指标。
- active topic 后续仍可以继续按 `$gframework-batch-boot 50` 推进,但应优先挑“已有 package README、但站内专题仍不足”的覆盖切片。 - active topic 后续仍可以继续按 `$gframework-batch-boot 50` 推进,但应优先挑“已有 package README、但站内专题仍不足”的覆盖切片。

View File

@ -26,6 +26,6 @@
## 2026-04-30 / RP-052 ## 2026-04-30 / RP-052
- 这一轮 coverage 扩展提交为 `f88f96c3`,提交后 branch diff 相对 `origin/main` 回落到 `8` files / `337` lines重新释放了 stop-condition 余量。 - 这一轮 coverage 扩展提交为 `f88f96c3`,提交后 branch diff 相对 `origin/main` 回落到 `8` files / `337` lines`,重新释放了 stop-condition 余量。
- active topic 后续仍可继续扩批,但更适合选择“已有 package README、但站内 docs 仍缺 reader-facing 专题”的切片,而不是继续给共享支撑层单开页面。 - active topic 后续仍可继续扩批,但更适合选择“已有 package README、但站内 docs 仍缺 reader-facing 专题”的切片,而不是继续给共享支撑层单开页面。
- 该状态也为后续用 `$gframework-pr-review` 精确跟进最新 review 线程提供了更清晰的恢复入口。 - 该状态也为后续用 `$gframework-pr-review` 精确跟进最新 review 线程提供了更清晰的恢复入口。

View File

@ -10,22 +10,19 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-055` - 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-053`
- 当前阶段:`Phase 5 - Governance Maintenance` - 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点: - 当前焦点:
- 处理 PR `#308` 当前 latest-head review 与 outside-diff review 中经本地复核仍成立的 reader-facing 文档与 active `ai-plan` 漂移问题 - 处理 PR `#308` latest-head review 中仍成立的 3 条 `CodeRabbit` open threads继续瘦身 active `ai-plan` 文档,并为 `Schema 配置生成器` 专题补集中式迁移与兼容性说明
- 当前事实: - 当前事实:
- `2026-05-01` 重新抓取 `$gframework-pr-review` 后确认PR `#308` 处于 `OPEN`latest reviewed commit 已前进到 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117` - `2026-05-01` 重新抓取 `$gframework-pr-review` 后确认PR `#308` 处于 `OPEN`latest reviewed commit 为 `097e97bcd66c89c79b4dafc30e24e8b650c7db63`
- 当前 `CodeRabbit` 仍有 `5` 条 latest-head open threads其中 `2` 条是本轮 latest review 针对 active tracking 与 `schema-config-generator.md` 措辞提出的新 actionable comments另有 `3` 条为已被当前文件内容满足但尚未由远端重新计算关闭的 stale 线程 - 当前 latest-head review 只剩 `CodeRabbit` `3` 条 open threads分别指向本 tracking、active trace 和 `docs/zh-CN/source-generators/schema-config-generator.md`
- `Greptile` / `Gemini Code Assist` 当前无 open threadCodeRabbit latest review 状态为 `CHANGES_REQUESTED` - GitHub Test Reporter 汇总为 `2222 passed / 0 failed`
- 本地复核后,最新 review 中真正仍成立的 `2` 条问题分别是active tracking 的 RP-055 验证引用仍指向 `RP-049``RP-052` 归档,以及 `schema-config-generator.md` 的“更稳妥地回退”措辞未同步
- `docs/zh-CN/source-generators/schema-config-generator.md` 已包含独立的“迁移与兼容性”章节,且运行时示例已补齐 `configRootPath` 定义,因此对应的 major / minor open threads 现阶段应视为 stale等待提交推送后再由远端重新计算
- GitHub Test Reporter 汇总为 `2247 passed / 0 failed`
- `Title check` 仍为 `Inconclusive`,属于 PR 元数据问题,不是仓库文件内可直接修复的阻塞项 - `Title check` 仍为 `Inconclusive`,属于 PR 元数据问题,不是仓库文件内可直接修复的阻塞项
- 本地修复尚未推送前,不会改变远端 latest reviewed commit 与 open-thread 统计 - 本地已完成 review 指向文件的收口,但在变更尚未提交推送前,重新抓取的 PR review 仍会继续显示旧的 latest reviewed commit 与同一批 open threads
- 当前风险: - 当前风险:
- 如果 active tracking / trace 继续保留旧的 commit SHA 和 review 归因,会让后续恢复点重复处理已经本地闭环的问题 - active tracking / trace 若继续保留阶段性细节与逐项验证,会再次偏离“快速恢复入口”的用途
- 在变更推送前PR 页面仍会继续展示 latest reviewed commit `00ecf6fb...` 下的 open-thread 数量,容易把 stale 线程误判为新的本地缺陷 - `Schema 配置生成器` 页已经说明边界,但迁移步骤、兼容边界和回退方式仍分散在多个段落中
## 当前状态摘要 ## 当前状态摘要
@ -59,12 +56,29 @@
## 最新验证 ## 最新验证
- `2026-05-01` 本轮关键验证结论:`$gframework-pr-review` 重新抓取通过,确认 PR `#308` 的 latest reviewed commit 为 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117``CodeRabbit` 当前仍有 `5` 条 latest-head open threads其中最新 review 的 `2` 条 actionable comments 已在当前工作树完成修复,其余 `3` 条因当前文件内容已满足而应视为 stale。两篇 source-generators 页面校验通过,`docs/` 站点构建通过并仅保留既有大 chunk warning。 - `2026-05-01` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/schema-config-generator.md`
- 本轮逐条命令与详细结果见 active trace 的 `RP-055` 验证段: - 结果:通过;新增“迁移与兼容性”小节后,页面的 frontmatter、链接与代码块校验仍然通过。
- `ai-plan/public/documentation-full-coverage-governance/traces/documentation-full-coverage-governance-trace.md` - `2026-05-01` `bun run build`(工作目录:`docs/`
- 结果通过active `ai-plan` 瘦身与 schema 专题页更新后站点仍可构建,仅保留既有大 chunk warning。
- `2026-05-01` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#308` 处于 `OPEN`latest-head review 当前只剩 `3``CodeRabbit` open threads测试汇总为 `2222 passed / 0 failed``Greptile` / `Gemini Code Assist` 当前无 open thread。
- `2026-05-01` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review-after-fix.json`
- 结果通过remote latest reviewed commit 仍是 `097e97bcd66c89c79b4dafc30e24e8b650c7db63`因此在本地改动尚未提交推送前PR 页面仍显示同一批 `3` 条 open threads。
- `2026-04-30` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/schema-config-generator.md`
- 结果:通过;`Schema 配置生成器` 专题页的 frontmatter、链接与代码块校验通过。
- `2026-04-30` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/index.md`
- 结果通过source-generators landing 的链接与结构校验通过。
- `2026-04-30` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 结果:通过;`Cqrs` generator 专题页在补足 fallback 精度说明后校验通过。
- `2026-04-30` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
- 结果:通过;`CQRS` 运行时页在补足 generated registry 协作说明后校验通过。
- `2026-04-30` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
- 结果通过API 入口页在补充 source-generator 阅读路线后校验通过。
- `2026-04-30` `bun run build`(工作目录:`docs/`
- 结果:通过;新增 source-generators 专题页与侧栏入口后站点仍可构建,仅保留既有大 chunk warning。
## 下一步 ## 下一步
1. 提交并推送本轮 source-generators 文档与 active `ai-plan` follow-up然后重新抓取 `$gframework-pr-review` 1. 完成本轮 3 个 latest-head review follow-up 后,重新运行最小文档验证,并重新抓取 `$gframework-pr-review`,确认 remote open threads 是否清空
2. 若 remote open threads 只剩 stale 的“迁移与兼容性”线程或 `Title check` 这类 metadata 项,则停止本轮仓库内修复。 2. 若 review 线程清空,再继续按 `$gframework-batch-boot 50` 挑选“已有 package README、但站内专题仍不足”的 coverage 切片,而不是继续扩写共享支撑层
3. 若 review 线程清空或只剩 metadata 项,再按 `$gframework-batch-boot 50` 继续挑选新的 coverage 切片,避免在同一轮混入无关改稿。 3. 若 `Title check` 仍保留,则单独修改 GitHub PR 标题,不把它和仓库文件内容混为同一类修复项

View File

@ -2,38 +2,32 @@
## 2026-05-01 ## 2026-05-01
### 当前恢复点RP-055 ### 当前恢复点RP-053
- 通过 `$gframework-pr-review` 重新抓取当前分支 PR `#308`,确认 latest reviewed commit 已前进到 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117`,当前 `CodeRabbit` 仍有 `5` 条 latest-head open threads。 - 通过 `$gframework-pr-review` 重新抓取当前分支 PR `#308`,确认 latest-head review 当前只剩 `3``CodeRabbit` open threads且都落在 active tracking、active trace 与 `Schema 配置生成器` 专题页。
- 本地复核确认最新 review 中真正仍成立的 `2` 条问题已经在当前工作树完成修复active tracking 的 RP-055 验证引用改为指向 active trace以及 `schema-config-generator.md` 的“更稳妥地回退”措辞同步。 - 本地复核确认这 3 条评论都仍成立active `ai-plan` 文档累积了过多阶段性细节,而 `schema-config-generator.md` 缺少集中式的迁移与兼容性小节。
- `schema-config-generator.md` 的“迁移与兼容性”章节与自包含运行时示例已在当前文件内容中满足 earlier open threads对应 `3` 条历史线程现阶段应视为 stale等待提交推送后由远端重新计算。 - GitHub Test Reporter 当前汇总为 `2222 passed / 0 failed``Title check` 仍然只是 PR 元数据问题,因此不纳入仓库文件修复范围。
- GitHub Test Reporter 当前汇总为 `2247 passed / 0 failed``Title check` 仍然只是 PR 元数据问题,因此不纳入仓库文件修复范围。
### 当前决策RP-055 ### 当前决策RP-053
- 补齐 `cqrs-handler-registry-generator.md` 的 fallback 条件说明,以及 `schema-config-generator.md` 的自包含运行时示例。 - active tracking 与 active trace 只保留当前恢复点、关键事实、风险、验证结论与下一步;`RP-049``RP-052` 的阶段细节迁入新的 archive 文件。
- active tracking 与 active trace 改写为与本次 PR 抓取一致的事实,修正 latest reviewed commit、线程数量、本地结论与验证摘要。 - `Schema 配置生成器` 页新增独立的“迁移与兼容性”小节,集中说明从手写注册迁移的最小步骤、当前兼容边界与回退做法。
- 只修仍然成立的 latest review 问题,不重复处理已被当前内容满足但尚未关闭的 stale 线程。 - 本轮只做 latest-head review 精确收口,不扩展到新的 docs coverage 批次。
- 本轮只做 PR review 精确收口,不扩展到新的 docs coverage 批次。
### 当前风险RP-055 ### 当前验证RP-053
- 在本轮提交推送前PR 页面仍会继续展示 latest reviewed commit `00ecf6fb...` 下的 open threads因此远端线程数量短时间内不会反映本地已修复状态。
- `schema-config-generator.md` 的“迁移与兼容性”线程和 `configRootPath` 线程已经由当前文件内容满足,但仍需依赖远端下一次 review 重新计算后才能从 open thread 列表中消失。
- `Title check` 仍是 PR 元数据层面的 `Inconclusive` 项;即使仓库文件全部修完,也不能仅凭本地改动让该检查转绿。
### 当前验证RP-055
- PR review 抓取: - PR review 抓取:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#308` 处于 `OPEN`latest reviewed commit 为 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117``CodeRabbit` 当前有 `5` 条 latest-head open threads其中最新 review 的 `2` 条 actionable comments 已本地修复,其余 `3` 条应视为 stale测试汇总为 `2247 passed / 0 failed`。 - 结果通过PR `#308` 处于 `OPEN`latest-head review 当前只剩 `3``CodeRabbit` open threads测试汇总为 `2222 passed / 0 failed`
- 页面校验: - 页面校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/schema-config-generator.md` - `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/schema-config-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` - 结果:通过;新增“迁移与兼容性”小节后页面校验通过。
- 结果通过fallback 条件说明与自包含示例改动后,两篇页面的 frontmatter、链接与代码块校验仍然通过。
- 站点构建: - 站点构建:
- `bun run build`(工作目录:`docs/` - `bun run build`(工作目录:`docs/`
- 结果通过source-generators 页面与 active `ai-plan` 更新后站点仍可构建,仅保留既有大 chunk warning。 - 结果通过active `ai-plan` 瘦身与 schema 专题页更新后站点仍可构建,仅保留既有大 chunk warning。
- 复核抓取:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review-after-fix.json`
- 结果:通过;由于 remote latest reviewed commit 仍未变化,本地未提交推送时 PR 页面仍显示同一批 `3` 条 open threads。
### 归档指针 ### 归档指针
@ -46,8 +40,8 @@
- `RP-049``RP-052` 的验证明细: - `RP-049``RP-052` 的验证明细:
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-049-to-rp-052-2026-05-01.md` `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-049-to-rp-052-2026-05-01.md`
### 下一步RP-055 ### 下一步RP-053
1. 提交并推送本轮 follow-up 后重新抓取 `$gframework-pr-review`,确认 remote open threads 是否只剩 stale 的 schema 线程或 `Title check` 1. 提交本轮 active `ai-plan` 瘦身与 `Schema 配置生成器` 迁移说明补充
2. 若只剩 metadata 项,则把后续动作限定为 GitHub PR 元数据修正,不继续在仓库里做无关变更 2. 运行最小文档验证并重新抓取 `$gframework-pr-review`,确认 latest-head review 是否已清空
3. 若 review 线程清空,则回到 `documentation-full-coverage-governance` 的下一个 coverage 切片,而不是继续在同一轮修改无关页面 3. 若只剩 `Title check`,则把后续动作限定为 GitHub PR 标题修正,不继续在仓库里做无关变更

View File

@ -165,29 +165,18 @@ protected override void OnInitialize()
1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute` 1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute`
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry` 2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
3. 当生成注册器同时暴露 generated request invoker provider 时runtime 会把 request/response 类型对对应的 descriptor 预先接线到 dispatcher 缓存,后续请求分发优先消费这些 generated request invoker 元数据 3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径
4. 当生成注册器同时暴露 generated stream invoker provider 时runtime 会以同样方式优先消费 stream request 对应的 generated stream invoker descriptor只有当前类型对未命中时才回退到既有反射 stream binding 4. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
5. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射绑定已命中的不兼容元数据会直接抛出异常 5. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
6. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler 6. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
7. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找 7. 同一程序集按稳定键去重,避免重复注册
8. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
9. 同一程序集按稳定键去重,避免重复注册
换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是生成注册器负责能静态表达的部分fallback 只补它覆盖不到的 handler。 换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是生成注册器负责能静态表达的部分fallback 只补它覆盖不到的 handler。
如果你在阅读 dispatcher 行为,可以把这部分理解成两组并列能力: 这套流程的关键点是:安装 `Cqrs.SourceGenerators` 后,不要求“所有 handler 都能被生成代码直接引用”才有收益。即使仍有 fallback
runtime 也会先消费 generated registry再只对剩余 handler 做定向补扫;只有旧版 marker 语义或空 fallback 元数据才会退回整程序集扫描。
- request invoker provider / descriptor 对接入方来说,`Type` fallback、按名称恢复的 fallback以及 mixed fallback 只影响补扫精度,不改变你的 `RegisterCqrsHandlersFromAssembly(...)`
- 面向 `SendRequestAsync(...)``SendAsync(...)``SendQueryAsync(...)` 这类单次请求分发 `RegisterCqrsHandlersFromAssemblies(...)` 接法。
- stream invoker provider / descriptor
- 面向 `CreateStream(...)` 触发的流式请求分发
两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。
对接入方来说,更关键的 reader-facing 语义是:安装 `Cqrs.SourceGenerators` 后,不要求“所有 handler 都能被生成代码直接引用”才有收益。
即使仍有 fallbackruntime 也会先消费 generated registry再只对剩余 handler 做定向补扫;只有旧版 marker 语义或空 fallback 元数据才会退回整程序集扫描。
`Type` fallback、按名称恢复的 fallback以及 mixed fallback 只影响补扫精度,不改变
`RegisterCqrsHandlersFromAssembly(...)``RegisterCqrsHandlersFromAssemblies(...)` 的接法。
`Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。 `Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。
@ -241,7 +230,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<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 |
| 运行时入口与内部协作层 | `CqrsRuntimeFactory``ICqrsHandlerRegistry``CqrsHandlerRegistryAttribute``CqrsReflectionFallbackAttribute``ICqrsRequestInvokerProvider`、`ICqrsStreamInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 | | `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory``ICqrsHandlerRegistry``CqrsHandlerRegistryAttribute``CqrsReflectionFallbackAttribute``DefaultCqrsRegistrationService` | runtime 创建入口、generated-registry 优先级、targeted fallback 语义和程序集去重规则 |
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator``RuntimeTypeReferenceSpec``OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 | | `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator``RuntimeTypeReferenceSpec``OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 |
## 继续阅读 ## 继续阅读

View File

@ -6,8 +6,7 @@ description: 为消费端程序集生成 CQRS handler registry并在需要时
# CQRS Handler Registry 生成器 # CQRS Handler Registry 生成器
`GFramework.Cqrs.SourceGenerators` 会在编译期为当前业务程序集生成 `ICqrsHandlerRegistry`,让 `GFramework.Cqrs` `GFramework.Cqrs.SourceGenerators` 会在编译期为当前业务程序集生成 `ICqrsHandlerRegistry`,让 `GFramework.Cqrs`
runtime 在注册 handlers 时优先走静态注册表;当运行时合同允许时,也会把 request / stream 分发可直接复用的 invoker runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整个程序集。
元数据前移到编译期,而不是总是先扫描整个程序集或在首次分发时再走反射绑定。
它服务的是 `Cqrs` 家族,不是独立运行时: 它服务的是 `Cqrs` 家族,不是独立运行时:
@ -28,17 +27,11 @@ runtime 在注册 handlers 时优先走静态注册表;当运行时合同允
1. 一个实现 `ICqrsHandlerRegistry` 的内部注册器类型 1. 一个实现 `ICqrsHandlerRegistry` 的内部注册器类型
2. 程序集级 `CqrsHandlerRegistryAttribute` 2. 程序集级 `CqrsHandlerRegistryAttribute`
当运行时暴露对应合同、且当前 handler 可被安全静态表达时,生成注册器还可以继续暴露:
- generated request invoker provider / descriptor
- generated stream invoker provider / descriptor
当某些 handler 不能被生成代码安全地直接引用时,还会补发: 当某些 handler 不能被生成代码安全地直接引用时,还会补发:
- 程序集级 `CqrsReflectionFallbackAttribute` - 程序集级 `CqrsReflectionFallbackAttribute`
这意味着运行时会先使用生成注册器完成可静态表达的映射;对 request 与 stream 分发来说,也会优先消费 generated invoker 这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。
descriptor。只有当前类型对没有 generated metadata或 registry / fallback 无法覆盖时,才继续回到既有反射 binding 或补扫路径,而不是退回整程序集盲扫。
如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。 如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。
## 最小接入路径 ## 最小接入路径
@ -85,17 +78,12 @@ RegisterCqrsHandlersFromAssemblies(
1. 先读取程序集上的 `CqrsHandlerRegistryAttribute` 1. 先读取程序集上的 `CqrsHandlerRegistryAttribute`
2. 优先激活生成的 `ICqrsHandlerRegistry` 2. 优先激活生成的 `ICqrsHandlerRegistry`
3. 若生成注册器同时提供 request invoker provider / descriptorregistrar 会把这些 request invoker 元数据预先登记到 dispatcher 缓存 3. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
4. 若生成注册器同时提供 stream invoker provider / descriptorruntime 也会优先消费对应的 generated stream invoker 元数据;未命中时仍回退到既有反射 stream binding 4. 若存在 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
5. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径 5. 同一程序集按稳定键去重,避免重复注册
6. 若存在 `CqrsReflectionFallbackAttribute`,优先按其中携带的 `Type` 或类型名补扫剩余 handler若元数据为空或只保留 marker 语义,则退回整程序集补扫
7. 同一程序集按稳定键去重,避免重复注册
这个行为由 这个行为由 `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs`
[运行时注册流程测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs) `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 共同覆盖。
[生成器行为测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs)
共同覆盖。
## 什么时候值得安装 ## 什么时候值得安装
@ -103,7 +91,6 @@ RegisterCqrsHandlersFromAssemblies(
- 业务程序集内 handler 数量较多 - 业务程序集内 handler 数量较多
- 想把 handler 注册路径前移到编译期 - 想把 handler 注册路径前移到编译期
- 想把 request / stream 分发里可静态确定的 invoker metadata 一并前移到编译期
- 希望冷启动阶段减少整程序集反射扫描 - 希望冷启动阶段减少整程序集反射扫描
- 需要更明确地观察“哪些 handler 走静态注册,哪些只能走 fallback” - 需要更明确地观察“哪些 handler 走静态注册,哪些只能走 fallback”
@ -175,8 +162,6 @@ RegisterCqrsHandlersFromAssemblies(
- `GFramework.Cqrs.ICqrsHandlerRegistry` - `GFramework.Cqrs.ICqrsHandlerRegistry`
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute` - `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute` - `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
- `GFramework.Cqrs.ICqrsRequestInvokerProvider`
- `GFramework.Cqrs.ICqrsStreamInvokerProvider`
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator` - `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
模块族入口见: 模块族入口见:

View File

@ -154,8 +154,6 @@ loader.RegisterAllGeneratedConfigTables();
using GFramework.Game.Config; using GFramework.Game.Config;
using GFramework.Game.Config.Generated; using GFramework.Game.Config.Generated;
var configRootPath = Path.Combine(AppContext.BaseDirectory, "schemas");
var bootstrap = new GameConfigBootstrap( var bootstrap = new GameConfigBootstrap(
new GameConfigBootstrapOptions new GameConfigBootstrapOptions
{ {
@ -182,7 +180,7 @@ var bootstrap = new GameConfigBootstrap(
- `x-gframework-index`、路径元数据和标识符映射都要求满足安全约束;旧项目里依赖宽松命名或不安全路径的配置,需要先整理 schema再切换到聚合注册入口。 - `x-gframework-index`、路径元数据和标识符映射都要求满足安全约束;旧项目里依赖宽松命名或不安全路径的配置,需要先整理 schema再切换到聚合注册入口。
- 迁移到生成链路后,运行时读取模型保持不变,但“表类型定义”和“注册目录”会改为编译期输出,因此自定义扩展更适合挂在单表绑定或 `ConfigureLoader` 阶段,而不是继续复制生成器会维护的样板代码。 - 迁移到生成链路后,运行时读取模型保持不变,但“表类型定义”和“注册目录”会改为编译期输出,因此自定义扩展更适合挂在单表绑定或 `ConfigureLoader` 阶段,而不是继续复制生成器会维护的样板代码。
如果你需要分批迁移,当前更稳妥地回退是只回退注册入口,而不是同时回退 schema 结构: 如果你需要分批迁移,当前更稳妥的回退方式是只回退注册入口,而不是同时回退 schema 结构:
- 可以先让一部分表继续沿手写注册保留,另一部分切到生成的单表绑定或聚合注册。 - 可以先让一部分表继续沿手写注册保留,另一部分切到生成的单表绑定或聚合注册。
- 如果某张表在迁移后触发诊断,优先根据诊断 ID 收敛 schema再决定是否临时回到手写注册。 - 如果某张表在迁移后触发诊断,优先根据诊断 ID 收敛 schema再决定是否临时回到手写注册。
@ -201,11 +199,7 @@ var bootstrap = new GameConfigBootstrap(
- 额外约束元数据不合法 - 额外约束元数据不合法
- 例如 `GF_ConfigSchema_008``GF_ConfigSchema_014` - 例如 `GF_ConfigSchema_008``GF_ConfigSchema_014`
这些边界由 这些边界由 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs` 和快照测试共同覆盖。遇到生成失败时,优先先看诊断 ID再回头核对 schema 本身是否超出当前公开子集。
[Schema 生成器行为测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs)
[生成结果快照测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs)
共同覆盖。遇到生成失败时,优先先看诊断 ID再回头核对 schema 本身是否超出当前公开子集。
## 什么时候优先看这页 ## 什么时候优先看这页