mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Compare commits
22 Commits
a468c9b4cd
...
6983b7ee84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6983b7ee84 | ||
|
|
00ecf6fb10 | ||
|
|
896e3efaa9 | ||
|
|
4fdb1e7398 | ||
|
|
241c9ffeb3 | ||
|
|
7e77fee0a5 | ||
|
|
103b961e6f | ||
|
|
1c21df1414 | ||
|
|
26314dba5e | ||
|
|
9296def108 | ||
|
|
83528742bb | ||
|
|
8b36626266 | ||
|
|
1091594224 | ||
|
|
502f65239c | ||
|
|
dc21188c79 | ||
|
|
6b5c5d9e2d | ||
|
|
5a77e2fb33 | ||
|
|
eb30388267 | ||
|
|
172c08176c | ||
|
|
ea0b937705 | ||
|
|
f17f9f3da6 | ||
|
|
98477068d6 |
@ -9,16 +9,23 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
string RequestTypeDisplayName,
|
||||
string ResponseTypeDisplayName);
|
||||
|
||||
private readonly record struct StreamInvokerRegistrationSpec(
|
||||
string RequestTypeDisplayName,
|
||||
string ResponseTypeDisplayName);
|
||||
|
||||
private readonly record struct HandlerRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string ImplementationTypeDisplayName,
|
||||
string HandlerInterfaceLogName,
|
||||
string ImplementationLogName,
|
||||
RequestInvokerRegistrationSpec? RequestInvokerRegistration);
|
||||
RequestInvokerRegistrationSpec? RequestInvokerRegistration,
|
||||
StreamInvokerRegistrationSpec? StreamInvokerRegistration);
|
||||
|
||||
private readonly record struct ReflectedImplementationRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string HandlerInterfaceLogName);
|
||||
string HandlerInterfaceLogName,
|
||||
RequestInvokerRegistrationSpec? RequestInvokerRegistration,
|
||||
StreamInvokerRegistrationSpec? StreamInvokerRegistration);
|
||||
|
||||
private readonly record struct OrderedRegistrationSpec(
|
||||
string HandlerInterfaceLogName,
|
||||
@ -31,7 +38,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
bool HasReflectionTypeLookups,
|
||||
bool HasExternalAssemblyTypeLookups,
|
||||
bool SupportsRequestInvokerProvider,
|
||||
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions)
|
||||
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions,
|
||||
bool SupportsStreamInvokerProvider,
|
||||
ImmutableArray<StreamInvokerEmissionSpec> StreamInvokerEmissions)
|
||||
{
|
||||
public bool RequiresRegistryAssemblyVariable =>
|
||||
HasReflectedImplementationRegistrations ||
|
||||
@ -39,6 +48,8 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
HasReflectionTypeLookups;
|
||||
|
||||
public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty;
|
||||
|
||||
public bool HasStreamInvokerProvider => SupportsStreamInvokerProvider && !StreamInvokerEmissions.IsDefaultOrEmpty;
|
||||
}
|
||||
|
||||
private readonly record struct RequestInvokerEmissionSpec(
|
||||
@ -47,6 +58,12 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
string HandlerInterfaceDisplayName,
|
||||
int MethodIndex);
|
||||
|
||||
private readonly record struct StreamInvokerEmissionSpec(
|
||||
string RequestTypeDisplayName,
|
||||
string ResponseTypeDisplayName,
|
||||
string HandlerInterfaceDisplayName,
|
||||
int MethodIndex);
|
||||
|
||||
/// <summary>
|
||||
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
||||
/// </summary>
|
||||
@ -328,5 +345,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
bool SupportsNamedReflectionFallbackTypes,
|
||||
bool SupportsDirectReflectionFallbackTypes,
|
||||
bool SupportsMultipleReflectionFallbackAttributes,
|
||||
bool SupportsRequestInvokerProvider);
|
||||
bool SupportsRequestInvokerProvider,
|
||||
bool SupportsStreamInvokerProvider);
|
||||
}
|
||||
|
||||
@ -57,6 +57,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
var requestInvokerEmissions = CreateRequestInvokerEmissions(
|
||||
generationEnvironment.SupportsRequestInvokerProvider,
|
||||
registrations);
|
||||
var streamInvokerEmissions = CreateStreamInvokerEmissions(
|
||||
generationEnvironment.SupportsStreamInvokerProvider,
|
||||
registrations);
|
||||
|
||||
return new GeneratedRegistrySourceShape(
|
||||
hasReflectedImplementationRegistrations,
|
||||
@ -64,11 +67,13 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
hasReflectionTypeLookups,
|
||||
hasExternalAssemblyTypeLookups,
|
||||
generationEnvironment.SupportsRequestInvokerProvider,
|
||||
requestInvokerEmissions);
|
||||
requestInvokerEmissions,
|
||||
generationEnvironment.SupportsStreamInvokerProvider,
|
||||
streamInvokerEmissions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 direct handler 注册描述中提取 request invoker 发射计划。
|
||||
/// 从可直接表达 handler 接口的注册描述中提取 request invoker 发射计划。
|
||||
/// </summary>
|
||||
/// <param name="supportsRequestInvokerProvider">
|
||||
/// 指示当前 runtime 是否同时暴露 <c>ICqrsRequestInvokerProvider</c> 与
|
||||
@ -76,7 +81,8 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// </param>
|
||||
/// <param name="registrations">已按稳定顺序整理完成的 handler 注册描述。</param>
|
||||
/// <returns>
|
||||
/// 由 <c>directRegistration.RequestInvokerRegistration</c> 派生出的 <see cref="RequestInvokerEmissionSpec" /> 集合。
|
||||
/// 由 direct registration 或 reflected-implementation registration 上的
|
||||
/// <c>RequestInvokerRegistration</c> 派生出的 <see cref="RequestInvokerEmissionSpec" /> 集合。
|
||||
/// <c>methodIndex</c> 按 <paramref name="registrations" /> 与其 direct registration 的遍历顺序单调递增,
|
||||
/// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。
|
||||
/// </returns>
|
||||
@ -106,6 +112,71 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
directRegistration.HandlerInterfaceDisplayName,
|
||||
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();
|
||||
@ -221,6 +292,15 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors");
|
||||
}
|
||||
|
||||
if (sourceShape.HasStreamInvokerProvider)
|
||||
{
|
||||
builder.Append(", global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".ICqrsStreamInvokerProvider, global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".IEnumeratesCqrsStreamInvokerDescriptors");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("{");
|
||||
AppendRegisterMethod(builder, registrations, sourceShape);
|
||||
@ -231,6 +311,12 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions);
|
||||
}
|
||||
|
||||
if (sourceShape.HasStreamInvokerProvider)
|
||||
{
|
||||
builder.AppendLine();
|
||||
AppendStreamInvokerProviderMembers(builder, sourceShape.StreamInvokerEmissions);
|
||||
}
|
||||
|
||||
if (sourceShape.HasExternalAssemblyTypeLookups)
|
||||
{
|
||||
builder.AppendLine();
|
||||
@ -366,9 +452,11 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// </remarks>
|
||||
private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
|
||||
{
|
||||
builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::");
|
||||
builder.Append(" global::System.Collections.Generic.IReadOnlyList<global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".CqrsRequestInvokerDescriptorEntry> GetDescriptors()");
|
||||
builder.Append(".CqrsRequestInvokerDescriptorEntry> global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return RequestInvokerDescriptors;");
|
||||
builder.AppendLine(" }");
|
||||
@ -424,6 +512,117 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 generated registry 的 stream invoker provider 成员。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="streamInvokerEmissions">当前要输出的 stream invoker 发射计划。</param>
|
||||
private static void AppendStreamInvokerProviderMembers(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<StreamInvokerEmissionSpec> streamInvokerEmissions)
|
||||
{
|
||||
AppendStreamInvokerDescriptorArray(builder, streamInvokerEmissions);
|
||||
builder.AppendLine();
|
||||
AppendStreamInvokerProviderMethods(builder);
|
||||
|
||||
for (var index = 0; index < streamInvokerEmissions.Length; index++)
|
||||
{
|
||||
builder.AppendLine();
|
||||
AppendStreamInvokerMethod(builder, streamInvokerEmissions[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 generated registry 的 stream invoker 描述符数组。
|
||||
/// </summary>
|
||||
private static void AppendStreamInvokerDescriptorArray(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<StreamInvokerEmissionSpec> streamInvokerEmissions)
|
||||
{
|
||||
builder.AppendLine(" private static readonly global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry[] StreamInvokerDescriptors =");
|
||||
builder.AppendLine(" [");
|
||||
|
||||
for (var index = 0; index < streamInvokerEmissions.Length; index++)
|
||||
{
|
||||
var emission = streamInvokerEmissions[index];
|
||||
builder.Append(" new global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsStreamInvokerDescriptorEntry(typeof(");
|
||||
builder.Append(emission.RequestTypeDisplayName);
|
||||
builder.Append("), typeof(");
|
||||
builder.Append(emission.ResponseTypeDisplayName);
|
||||
builder.Append("), new global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsStreamInvokerDescriptor(typeof(");
|
||||
builder.Append(emission.HandlerInterfaceDisplayName);
|
||||
builder.Append("), typeof(");
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.Append(").GetMethod(nameof(InvokeStreamHandler");
|
||||
builder.Append(emission.MethodIndex);
|
||||
builder.Append("), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))");
|
||||
builder.AppendLine(index == streamInvokerEmissions.Length - 1 ? string.Empty : ",");
|
||||
}
|
||||
|
||||
builder.AppendLine(" ];");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 generated registry 对 stream invoker provider 契约的实现方法。
|
||||
/// </summary>
|
||||
private static void AppendStreamInvokerProviderMethods(StringBuilder builder)
|
||||
{
|
||||
builder.Append(" global::System.Collections.Generic.IReadOnlyList<global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsStreamInvokerDescriptorEntry> global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".IEnumeratesCqrsStreamInvokerDescriptors.GetDescriptors()");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return StreamInvokerDescriptors;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.Append(" public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".CqrsStreamInvokerDescriptor? descriptor)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" if (requestType is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(requestType));");
|
||||
builder.AppendLine(" if (responseType is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(responseType));");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" foreach (var entry in StreamInvokerDescriptors)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" if (entry.RequestType == requestType && entry.ResponseType == responseType)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" descriptor = entry.Descriptor;");
|
||||
builder.AppendLine(" return true;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" descriptor = null;");
|
||||
builder.AppendLine(" return false;");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为单个 stream invoker 描述符发射对应的静态强类型桥接方法。
|
||||
/// </summary>
|
||||
private static void AppendStreamInvokerMethod(StringBuilder builder, StreamInvokerEmissionSpec emission)
|
||||
{
|
||||
builder.Append(" private static object InvokeStreamHandler");
|
||||
builder.Append(emission.MethodIndex);
|
||||
builder.Append("(object handler, object request, global::System.Threading.CancellationToken cancellationToken)");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" {");
|
||||
builder.Append(" var typedHandler = (");
|
||||
builder.Append(emission.HandlerInterfaceDisplayName);
|
||||
builder.AppendLine(")handler;");
|
||||
builder.Append(" var typedRequest = (");
|
||||
builder.Append(emission.RequestTypeDisplayName);
|
||||
builder.AppendLine(")request;");
|
||||
builder.AppendLine(" return typedHandler.Handle(typedRequest, cancellationToken);");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void AppendDirectRegistrations(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration)
|
||||
|
||||
@ -22,6 +22,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor";
|
||||
private const string CqrsRequestInvokerDescriptorEntryMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry";
|
||||
private const string ICqrsStreamInvokerProviderMetadataName = $"{CqrsRuntimeNamespace}.ICqrsStreamInvokerProvider";
|
||||
private const string IEnumeratesCqrsStreamInvokerDescriptorsMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.IEnumeratesCqrsStreamInvokerDescriptors";
|
||||
private const string CqrsStreamInvokerDescriptorMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsStreamInvokerDescriptor";
|
||||
private const string CqrsStreamInvokerDescriptorEntryMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsStreamInvokerDescriptorEntry";
|
||||
|
||||
private const string CqrsHandlerRegistryAttributeMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
||||
@ -78,6 +85,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) is not null;
|
||||
var supportsStreamInvokerProvider =
|
||||
compilation.GetTypeByMetadataName(ICqrsStreamInvokerProviderMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(IEnumeratesCqrsStreamInvokerDescriptorsMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(CqrsStreamInvokerDescriptorMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(CqrsStreamInvokerDescriptorEntryMetadataName) is not null;
|
||||
var stringType = compilation.GetSpecialType(SpecialType.System_String);
|
||||
var typeType = compilation.GetTypeByMetadataName("System.Type");
|
||||
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
|
||||
@ -98,7 +110,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
supportsNamedReflectionFallbackTypes,
|
||||
supportsDirectReflectionFallbackTypes,
|
||||
supportsMultipleReflectionFallbackAttributes,
|
||||
supportsRequestInvokerProvider);
|
||||
supportsRequestInvokerProvider,
|
||||
supportsStreamInvokerProvider);
|
||||
}
|
||||
|
||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||
@ -234,6 +247,9 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
implementationLogName,
|
||||
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
|
||||
? requestInvokerRegistration
|
||||
: null,
|
||||
TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration)
|
||||
? streamInvokerRegistration
|
||||
: null));
|
||||
return true;
|
||||
}
|
||||
@ -242,7 +258,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
{
|
||||
reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec(
|
||||
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
GetLogDisplayName(handlerInterface)));
|
||||
GetLogDisplayName(handlerInterface),
|
||||
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
|
||||
? requestInvokerRegistration
|
||||
: null,
|
||||
TryCreateStreamInvokerRegistrationSpec(handlerInterface, out var streamInvokerRegistration)
|
||||
? streamInvokerRegistration
|
||||
: null));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -281,6 +303,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当当前直接注册项属于流式请求处理器时,提取 stream invoker provider 所需的请求/响应类型显示名。
|
||||
/// </summary>
|
||||
private static bool TryCreateStreamInvokerRegistrationSpec(
|
||||
INamedTypeSymbol handlerInterface,
|
||||
out StreamInvokerRegistrationSpec streamInvokerRegistration)
|
||||
{
|
||||
if (!string.Equals(
|
||||
handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
$"global::{CqrsContractsNamespace}.IStreamRequestHandler<TRequest, TResponse>",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
streamInvokerRegistration = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (handlerInterface.TypeArguments.Length != 2)
|
||||
{
|
||||
streamInvokerRegistration = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
streamInvokerRegistration = new StreamInvokerRegistrationSpec(
|
||||
handlerInterface.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
handlerInterface.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
|
||||
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
并生成:
|
||||
|
||||
- `ICqrsHandlerRegistry` 实现
|
||||
- 在运行时合同允许时,额外生成 request / stream invoker provider 与 descriptor 元数据
|
||||
- 程序集级 `CqrsHandlerRegistryAttribute`
|
||||
- 必要时的 `CqrsReflectionFallbackAttribute` 元数据
|
||||
|
||||
@ -34,6 +35,8 @@
|
||||
|
||||
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler,则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
|
||||
当 fallback handler 本身仍可直接引用时,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;如果 runtime 允许同一程序集声明多个 fallback 特性实例,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少运行时按类型名回查程序集的成本。
|
||||
当 runtime 同时暴露 request / stream invoker provider 契约时,生成注册器还会为可直接静态表达的 `IRequestHandler<,>` 与
|
||||
`IStreamRequestHandler<,>` 发射对应 descriptor 与开放静态 invoker 方法,让 runtime 在首次创建 request / stream binding 时优先消费这些编译期元数据;未命中时仍保持既有反射 binding 创建语义。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
@ -55,6 +58,7 @@ RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly);
|
||||
```
|
||||
|
||||
安装生成器后,运行时会优先走生成的 registry;无法静态表达的部分再走定向回退。
|
||||
如果当前 runtime 合同已经包含 request / stream invoker provider seam,generated registry 还会把这两类 invoker 元数据一并前移到编译期。
|
||||
|
||||
## 什么时候值得安装
|
||||
|
||||
|
||||
@ -57,6 +57,62 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
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>
|
||||
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
|
||||
/// </summary>
|
||||
@ -74,6 +130,637 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
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>
|
||||
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
||||
/// </summary>
|
||||
@ -89,6 +776,72 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带有 generated stream invoker registry 元数据的程序集替身。
|
||||
/// </summary>
|
||||
private static Mock<Assembly> CreateGeneratedStreamInvokerAssembly()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns("GFramework.Cqrs.Tests.Cqrs.GeneratedStreamInvokerAssembly, Version=1.0.0.0");
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedStreamInvokerProviderRegistry))]);
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带有指定 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>
|
||||
/// 清空 registrar 静态缓存。
|
||||
/// </summary>
|
||||
@ -109,6 +862,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
ClearCache(GetDispatcherCacheField("RequestDispatchBindings"));
|
||||
ClearCache(GetDispatcherCacheField("StreamDispatchBindings"));
|
||||
ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers"));
|
||||
ClearCache(GetDispatcherCacheField("GeneratedStreamInvokers"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -149,4 +903,22 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
.Invoke(cache, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 枚举并收集当前异步流中的全部元素,便于断言 generated stream invoker 的输出。
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">流元素类型。</typeparam>
|
||||
/// <param name="stream">待消耗的异步流。</param>
|
||||
/// <returns>按产出顺序收集得到的元素列表。</returns>
|
||||
private static async Task<IReadOnlyList<TItem>> DrainAsync<TItem>(IAsyncEnumerable<TItem> stream)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
var items = new List<TItem>();
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 模拟同时提供 handler 注册与 stream invoker 元数据的 generated registry。
|
||||
/// </summary>
|
||||
internal sealed class GeneratedStreamInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsStreamInvokerProvider,
|
||||
IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
|
||||
typeof(GeneratedStreamInvokerRequest),
|
||||
typeof(int),
|
||||
Descriptor);
|
||||
|
||||
/// <summary>
|
||||
/// 将测试流式请求处理器注册到目标服务集合。
|
||||
/// </summary>
|
||||
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||
/// <param name="logger">用于记录注册诊断的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(GeneratedStreamInvokerRequestHandler));
|
||||
logger.Debug(
|
||||
$"Registered CQRS handler {typeof(GeneratedStreamInvokerRequestHandler).FullName} as {typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>).FullName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试返回指定 stream request/response 类型对对应的 generated invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <param name="descriptor">命中时返回的描述符。</param>
|
||||
/// <returns>若类型对匹配当前测试流式请求则返回 <see langword="true" />。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 registry 暴露的全部 generated stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <returns>单条测试 stream invoker 描述符条目。</returns>
|
||||
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return [DescriptorEntry];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated stream invoker 直接执行后的返回值。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前流式请求处理器实例。</param>
|
||||
/// <param name="request">当前测试流式请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>带有 generated 语义的异步流,便于断言 dispatcher 走了 provider 路径。</returns>
|
||||
private static object InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = handler as IStreamRequestHandler<GeneratedStreamInvokerRequest, int>
|
||||
?? throw new InvalidOperationException("Generated stream invoker received an incompatible handler instance.");
|
||||
var typedRequest = (GeneratedStreamInvokerRequest)request;
|
||||
return StreamResultsAsync(typedRequest.Start, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造供测试断言使用的固定异步流结果。
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<int> StreamResultsAsync(
|
||||
int start,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
yield return start * 10;
|
||||
await Task.Yield();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return start * 10 + 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 generated stream invoker provider 接线的测试流式请求。
|
||||
/// </summary>
|
||||
/// <param name="Start">用于构造 generated stream 输出的起始值。</param>
|
||||
internal sealed record GeneratedStreamInvokerRequest(int Start) : IStreamRequest<int>;
|
||||
@ -0,0 +1,34 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 供 generated stream invoker provider 测试使用的流式请求处理器。
|
||||
/// </summary>
|
||||
internal sealed class GeneratedStreamInvokerRequestHandler : IStreamRequestHandler<GeneratedStreamInvokerRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回带有运行时处理器语义的异步流,便于和 generated invoker 自定义结果区分。
|
||||
/// </summary>
|
||||
/// <param name="request">当前测试流式请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>运行时处理器生成的异步流结果。</returns>
|
||||
public IAsyncEnumerable<int> Handle(GeneratedStreamInvokerRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return StreamResultsAsync(request.Start, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成用于区分 runtime 路径的固定异步流结果。
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<int> StreamResultsAsync(
|
||||
int start,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
yield return start;
|
||||
await Task.Yield();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return start + 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
||||
@ -19,6 +19,9 @@ public sealed class CqrsRequestInvokerDescriptor(
|
||||
Type handlerType,
|
||||
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>
|
||||
@ -27,5 +30,22 @@ public sealed class CqrsRequestInvokerDescriptor(
|
||||
/// <summary>
|
||||
/// 获取执行请求处理器的开放静态方法。
|
||||
/// </summary>
|
||||
public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,39 @@ namespace GFramework.Cqrs;
|
||||
/// <summary>
|
||||
/// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。
|
||||
/// </summary>
|
||||
/// <param name="RequestType">请求运行时类型。</param>
|
||||
/// <param name="ResponseType">响应运行时类型。</param>
|
||||
/// <param name="Descriptor">对应的 generated request invoker 描述符。</param>
|
||||
public sealed record CqrsRequestInvokerDescriptorEntry(
|
||||
Type RequestType,
|
||||
Type ResponseType,
|
||||
CqrsRequestInvokerDescriptor Descriptor);
|
||||
public sealed record CqrsRequestInvokerDescriptorEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化 request invoker 描述符映射条目。
|
||||
/// </summary>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <param name="responseType">响应运行时类型。</param>
|
||||
/// <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; }
|
||||
}
|
||||
|
||||
51
GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs
Normal file
51
GFramework.Cqrs/CqrsStreamInvokerDescriptor.cs
Normal file
@ -0,0 +1,51 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
41
GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs
Normal file
41
GFramework.Cqrs/CqrsStreamInvokerDescriptorEntry.cs
Normal file
@ -0,0 +1,41 @@
|
||||
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; }
|
||||
}
|
||||
33
GFramework.Cqrs/ICqrsStreamInvokerProvider.cs
Normal file
33
GFramework.Cqrs/ICqrsStreamInvokerProvider.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 定义由源码生成器或手写注册器提供的 stream invoker 元数据契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该 seam 允许运行时在首次创建 stream dispatch binding 时,
|
||||
/// 直接复用编译期已知的流式请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。
|
||||
/// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射 binding 创建路径。
|
||||
/// 当前默认 runtime 通过 <see cref="IEnumeratesCqrsStreamInvokerDescriptors" /> 在注册阶段一次性读取并缓存
|
||||
/// provider 暴露的描述符;<see cref="TryGetDescriptor(Type, Type, out CqrsStreamInvokerDescriptor?)" />
|
||||
/// 主要用于 provider 自检、测试和显式调用场景,而不是 dispatcher 在建流热路径上的二次回调入口。
|
||||
/// </remarks>
|
||||
public interface ICqrsStreamInvokerProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试为指定流式请求/响应类型对提供运行时元数据。
|
||||
/// </summary>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 stream invoker 元数据。</param>
|
||||
/// <returns>若当前 provider 可处理该流式请求/响应类型对则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
/// <remarks>
|
||||
/// 若 provider 希望被默认 runtime 自动接线到 dispatcher 的 generated invoker 缓存中,
|
||||
/// 还必须同时实现 <see cref="IEnumeratesCqrsStreamInvokerDescriptors" />,以便 registrar 在注册阶段枚举全部描述符。
|
||||
/// </remarks>
|
||||
bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsStreamInvokerDescriptor? descriptor);
|
||||
}
|
||||
18
GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs
Normal file
18
GFramework.Cqrs/IEnumeratesCqrsStreamInvokerDescriptors.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 generated stream invoker provider 暴露可枚举描述符集合的内部辅助契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 stream invoker 描述符,
|
||||
/// 并把它们登记到 dispatcher 的进程级弱缓存中。
|
||||
/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。
|
||||
/// </remarks>
|
||||
public interface IEnumeratesCqrsStreamInvokerDescriptors
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回当前 provider 可声明的全部 stream invoker 描述符条目。
|
||||
/// </summary>
|
||||
/// <returns>按 provider 定义顺序枚举的描述符条目集合。</returns>
|
||||
IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors();
|
||||
}
|
||||
@ -23,6 +23,11 @@ internal sealed class CqrsDispatcher(
|
||||
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
|
||||
GeneratedRequestInvokers = new();
|
||||
|
||||
// 卸载安全的进程级缓存:当 generated registry 提供 stream invoker 元数据时,
|
||||
// registrar 会按流式请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
|
||||
private static readonly WeakTypePairCache<GeneratedStreamInvokerMetadata>
|
||||
GeneratedStreamInvokers = new();
|
||||
|
||||
// 卸载安全的进程级缓存:通知类型只以弱键语义保留。
|
||||
// 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
|
||||
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
|
||||
@ -251,14 +256,23 @@ 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}.");
|
||||
}
|
||||
|
||||
if (Delegate.CreateDelegate(typeof(RequestInvoker<TResponse>), descriptor.InvokerMethod) is not
|
||||
RequestInvoker<TResponse> invoker)
|
||||
try
|
||||
{
|
||||
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(
|
||||
$"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>
|
||||
@ -276,11 +290,71 @@ internal sealed class CqrsDispatcher(
|
||||
/// </summary>
|
||||
private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType)
|
||||
{
|
||||
var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor(requestType, responseType);
|
||||
if (generatedDescriptor is not null)
|
||||
{
|
||||
var resolvedGeneratedDescriptor = generatedDescriptor.Value;
|
||||
return new StreamDispatchBinding(
|
||||
resolvedGeneratedDescriptor.HandlerType,
|
||||
resolvedGeneratedDescriptor.Invoker);
|
||||
}
|
||||
|
||||
return new StreamDispatchBinding(
|
||||
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType),
|
||||
CreateStreamInvoker(requestType, responseType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从容器已注册的 generated stream invoker provider 中获取指定流式请求/响应类型对的元数据。
|
||||
/// </summary>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <returns>命中时返回强类型化后的描述符;否则返回 <see langword="null" />。</returns>
|
||||
private static StreamInvokerDescriptor? TryGetGeneratedStreamInvokerDescriptor(Type requestType, Type responseType)
|
||||
{
|
||||
return GeneratedStreamInvokers.TryGetValue(requestType, responseType, out var metadata) &&
|
||||
metadata is not null
|
||||
? CreateStreamInvokerDescriptor(requestType, responseType, metadata)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的 stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <param name="descriptor">provider 返回的弱类型描述符。</param>
|
||||
/// <returns>可直接用于创建 stream dispatch binding 的描述符。</returns>
|
||||
/// <exception cref="InvalidOperationException">当 provider 返回的委托签名与当前流式请求/响应类型对不匹配时抛出。</exception>
|
||||
private static StreamInvokerDescriptor CreateStreamInvokerDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
GeneratedStreamInvokerMetadata descriptor)
|
||||
{
|
||||
if (!descriptor.InvokerMethod.IsStatic)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}.");
|
||||
}
|
||||
|
||||
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>
|
||||
@ -646,6 +720,15 @@ internal sealed class CqrsDispatcher(
|
||||
Type HandlerType,
|
||||
MethodInfo InvokerMethod);
|
||||
|
||||
/// <summary>
|
||||
/// 记录 registrar 写入的 generated stream invoker 元数据。
|
||||
/// </summary>
|
||||
/// <param name="HandlerType">流式请求处理器在容器中的服务类型。</param>
|
||||
/// <param name="InvokerMethod">执行流式请求处理器的开放静态方法。</param>
|
||||
private sealed record GeneratedStreamInvokerMetadata(
|
||||
Type HandlerType,
|
||||
MethodInfo InvokerMethod);
|
||||
|
||||
/// <summary>
|
||||
/// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。
|
||||
/// </summary>
|
||||
@ -654,6 +737,15 @@ internal sealed class CqrsDispatcher(
|
||||
Type HandlerType,
|
||||
RequestInvoker<TResponse> Invoker);
|
||||
|
||||
/// <summary>
|
||||
/// 保存 provider 返回的流式请求处理器服务类型与 stream invoker。
|
||||
/// </summary>
|
||||
/// <param name="HandlerType">流式请求处理器在容器中的服务类型。</param>
|
||||
/// <param name="Invoker">执行流式请求处理器的调用委托。</param>
|
||||
private readonly record struct StreamInvokerDescriptor(
|
||||
Type HandlerType,
|
||||
StreamInvoker Invoker);
|
||||
|
||||
/// <summary>
|
||||
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
|
||||
/// </summary>
|
||||
@ -677,6 +769,29 @@ internal sealed class CqrsDispatcher(
|
||||
descriptor.InvokerMethod));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 registrar 在 generated registry 激活后登记 stream invoker 元数据。
|
||||
/// </summary>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <param name="descriptor">要登记的 generated stream invoker 描述符。</param>
|
||||
internal static void RegisterGeneratedStreamInvokerDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
CqrsStreamInvokerDescriptor descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
|
||||
_ = GeneratedStreamInvokers.GetOrAdd(
|
||||
requestType,
|
||||
responseType,
|
||||
(_, _) => new GeneratedStreamInvokerMetadata(
|
||||
descriptor.HandlerType,
|
||||
descriptor.InvokerMethod));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
|
||||
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。
|
||||
|
||||
@ -240,6 +240,7 @@ internal static class CqrsHandlerRegistrar
|
||||
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
|
||||
registry.Register(services, logger);
|
||||
RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger);
|
||||
RegisterGeneratedStreamInvokerProvider(services, registry, assemblyName, logger);
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,6 +299,61 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 generated registry 同时提供 stream invoker 元数据时,把该 provider 注册到当前容器中。
|
||||
/// </summary>
|
||||
/// <param name="services">目标服务集合。</param>
|
||||
/// <param name="registry">当前已激活的 generated registry。</param>
|
||||
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <remarks>
|
||||
/// provider 作为 registry 的附加能力注册到容器后,dispatcher 才能在首次建流时优先消费编译期生成的 invoker 元数据。
|
||||
/// 若 registry 不实现该契约,则保持现有纯反射 stream binding 创建语义。
|
||||
/// </remarks>
|
||||
private static void RegisterGeneratedStreamInvokerProvider(
|
||||
IServiceCollection services,
|
||||
ICqrsHandlerRegistry registry,
|
||||
string assemblyName,
|
||||
ILogger logger)
|
||||
{
|
||||
if (registry is not ICqrsStreamInvokerProvider provider)
|
||||
return;
|
||||
|
||||
RegisterGeneratedStreamInvokerDescriptors(provider, assemblyName, logger);
|
||||
services.AddSingleton(typeof(ICqrsStreamInvokerProvider), provider);
|
||||
logger.Debug(
|
||||
$"Registered CQRS stream invoker provider {provider.GetType().FullName} for assembly {assemblyName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 generated stream invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。
|
||||
/// </summary>
|
||||
/// <param name="provider">当前已激活的 stream invoker provider。</param>
|
||||
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <remarks>
|
||||
/// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。
|
||||
/// 这样 stream dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。
|
||||
/// </remarks>
|
||||
private static void RegisterGeneratedStreamInvokerDescriptors(
|
||||
ICqrsStreamInvokerProvider provider,
|
||||
string assemblyName,
|
||||
ILogger logger)
|
||||
{
|
||||
if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource)
|
||||
return;
|
||||
|
||||
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
|
||||
{
|
||||
CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType,
|
||||
descriptorEntry.Descriptor);
|
||||
logger.Debug(
|
||||
$"Registered generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from assembly {assemblyName}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
||||
/// </summary>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
- `GeWuYou.GFramework.Cqrs`
|
||||
- 默认 runtime 与业务侧常用基类。
|
||||
- `GeWuYou.GFramework.Cqrs.SourceGenerators`
|
||||
- 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。
|
||||
- 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,并在可用时补充 generated request / stream invoker provider 元数据;运行时会优先消费这些编译期元数据,只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径。
|
||||
- `GFramework.Core`
|
||||
- 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。
|
||||
|
||||
@ -120,13 +120,15 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
|
||||
## 运行时行为
|
||||
|
||||
- 请求分发
|
||||
- `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,未找到处理器会抛出异常。
|
||||
- `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,若当前程序集提供 generated request invoker provider,则会先复用对应 descriptor 中的处理器服务类型与 invoker 元数据;未命中时仍回退到既有反射 request binding 创建路径。
|
||||
- 未找到处理器会抛出异常。
|
||||
- 通知分发
|
||||
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
|
||||
- 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。
|
||||
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。
|
||||
- 流式请求
|
||||
- 通过 `IStreamRequest<TResponse>` 和 `IStreamRequestHandler<,>` 返回 `IAsyncEnumerable<TResponse>`。
|
||||
- 当消费端程序集提供 generated stream invoker provider / descriptor 后,runtime 会优先消费这组 stream invoker 元数据;未命中时仍回退到既有反射 stream binding 创建路径。
|
||||
- 上下文注入
|
||||
- 处理器基类继承 `CqrsContextAwareHandlerBase`,runtime 会在分发前注入当前 `IArchitectureContext`。
|
||||
- 如果处理器或行为需要上下文注入,而当前 `ICqrsContext` 不是 `IArchitectureContext`,默认实现会抛出异常。
|
||||
@ -140,6 +142,7 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
|
||||
|
||||
- 同一程序集按稳定键去重,避免重复注册。
|
||||
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
|
||||
- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时,registrar 会把对应 descriptor 元数据接线到 runtime 缓存。
|
||||
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
|
||||
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
|
||||
- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
|
||||
|
||||
@ -1790,6 +1790,534 @@ 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>
|
||||
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
|
||||
/// </summary>
|
||||
@ -2383,7 +2911,322 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"public global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()"));
|
||||
"global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors.GetDescriptors()"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <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"));
|
||||
});
|
||||
}
|
||||
|
||||
@ -2438,6 +3281,95 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
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>
|
||||
/// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
# 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` 的回归
|
||||
@ -0,0 +1,34 @@
|
||||
# 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,后续若需追溯阶段细节,应同时参考对应测试文件、提交记录与本归档摘要。
|
||||
@ -7,267 +7,54 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-067`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-076`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前焦点:
|
||||
- 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
|
||||
- 当前评估结论已明确:`GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,但仓库内部旧总线 API、
|
||||
兼容 seam、fallback 旧语义与测试命名仍未完全收口
|
||||
- 当前评估结论已明确:相对 `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 锚点:`PR #307`
|
||||
- 当前结论:
|
||||
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
|
||||
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,当前 `RP-076` 已补齐 stream invoker provider gate 的四项 runtime 合同分支
|
||||
- `ai-plan` active 入口现以 `PR #307` 和 `RP-076` 为唯一权威恢复锚点;更早 PR 与阶段细节均以下方归档为准
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- `Phase 8` 仍是当前主线,不再回退到 `Phase 7`
|
||||
- `2026-04-20` 已重新执行 `$gframework-pr-review`:
|
||||
- 当前分支对应 `PR #261`,状态为 `OPEN`
|
||||
- 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` passed;MegaLinter 仅剩 `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` passed,MegaLinter 仍只暴露 `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 进入中期候选
|
||||
- 当前分支对应 `PR #307`,状态为 `OPEN`
|
||||
- latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛
|
||||
- 远端 `CTRF` 最新汇总为 `2247/2247` passed
|
||||
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响
|
||||
- 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖
|
||||
- 当前对外替代已基本完成,但若不单独规划旧 `Command` / `Query`、`LegacyICqrsRuntime` 与测试命名的收口顺序,
|
||||
后续仍会持续混淆“生产替代已完成”与“仓库内部收口未完成”这两个不同结论
|
||||
- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证
|
||||
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
|
||||
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
|
||||
|
||||
## 最近权威验证
|
||||
|
||||
- `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-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)
|
||||
- 历史 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-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档
|
||||
- `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 与专门测试,可暂时移出最高优先级
|
||||
- `PR #261`、`PR #302`、`PR #305` 及更早阶段的详细过程已不再作为 active 恢复入口;如需追溯,以对应归档文件为准
|
||||
- active tracking 仅保留当前恢复点、当前风险、最近权威验证与下一推荐步骤,避免 `boot` 落到历史阶段细节
|
||||
|
||||
@ -2,169 +2,31 @@
|
||||
|
||||
## 2026-04-30
|
||||
|
||||
### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067)
|
||||
### 阶段:PR #307 active 入口收敛(CQRS-REWRITE-RP-076)
|
||||
|
||||
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
|
||||
- 在 `RP-066` 提交后复算 branch diff,相对 `origin/main` 增长到 `22 files`,仍明显低于 `50 files` stop condition,因此继续下一批
|
||||
- 本轮 critical path 保持在主线程,本地完成 `dispatch/invoker` 生成前移的最小 request 切片;尝试委派 source-generator 测试给 worker 时因 subagent 名额已满失败,因此主线程直接接管该测试修改
|
||||
- 本轮关键设计调整:
|
||||
- 不按 `requestType.Assembly` 做 provider 发现,避免“请求定义在 A、handler 与 generated registry 在 B”时漏掉 generated invoker
|
||||
- generated registry 若实现 `ICqrsRequestInvokerProvider`,registrar 会在激活 registry 后把 provider 注册进容器,并通过 `IEnumeratesCqrsRequestInvokerDescriptors` 把描述符写入 dispatcher 的进程级弱缓存
|
||||
- dispatcher 首次创建 request dispatch binding 时只按 `requestType + responseType` 读取静态弱缓存,不依赖具体容器实例;未命中时仍走既有反射创建路径
|
||||
- 已完成实现:
|
||||
- `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、
|
||||
`CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry`
|
||||
- `CqrsHandlerRegistrar` 现会识别 generated registry 的 request invoker provider 能力,并登记 provider 与 request invoker 描述符
|
||||
- `CqrsDispatcher` 新增 generated request invoker 弱缓存,并在 request binding 创建时优先消费该元数据
|
||||
- `CqrsHandlerRegistryGenerator` 在 runtime 合同可用时,会让 generated registry 额外实现 request invoker provider 相关接口,并发射 descriptor 列表、`TryGetDescriptor(...)`、`GetDescriptors()` 与 request invoker 静态方法
|
||||
- 已补充测试:
|
||||
- `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider,且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果
|
||||
- `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法
|
||||
- 继续沿用 `$gframework-pr-review` 对 `PR #307` 做 latest-head triage,本轮只处理仍成立的 `ai-plan` 恢复入口问题
|
||||
- 主线程确认当前远端权威信号:
|
||||
- 当前分支对应 `PR #307`,状态为 `OPEN`
|
||||
- 远端 `CTRF` 最新汇总为 `2247/2247` passed
|
||||
- `MegaLinter` 仅剩 `dotnet-format` 的 `Restore operation failed` 环境噪音
|
||||
- 仍未闭环的 review 重点集中在 `cqrs-rewrite` active tracking / trace 仍保留过多历史锚点,而非新的运行时代码缺陷
|
||||
- 本轮决策:
|
||||
- 将 active tracking 收敛为单一恢复入口,只保留 `RP-076`、`PR #307`、活跃风险、最近权威验证与下一推荐步骤
|
||||
- 将 active trace 收敛为当前阶段的关键事实与决策,不再在默认恢复入口中保留 `RP-062` 之后的长阶段流水账
|
||||
- 新增 `archive/traces/cqrs-rewrite-history-rp062-through-rp076.md` 承接 `RP-062` 至 `RP-076` 的详细 trace 历史,保持旧阶段仍可追溯
|
||||
|
||||
### 验证(RP-067)
|
||||
### 验证(RP-076)
|
||||
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
|
||||
- 结果:通过,`22/22` passed
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
|
||||
- 结果:通过,`1/1` passed
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
|
||||
### 当前下一步(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
|
||||
- `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`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过
|
||||
|
||||
### 当前下一步(RP-064)
|
||||
|
||||
1. 评估 notification publisher seam 的第二阶段是否需要公开配置面、并行 publisher 或 telemetry decorator
|
||||
2. 把 `dispatch/invoker` 生成前移重新拉回 `Phase 8` 主线,作为下一个实现切片
|
||||
|
||||
### 阶段: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`
|
||||
- 备注:确认 `PR #307` 的当前 review 重点已收敛到 `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
|
||||
|
||||
## 活跃事实
|
||||
### 当前下一步(RP-076)
|
||||
|
||||
- 当前主题仍处于 `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` 生成前移为优先对象,形成下一轮可执行设计
|
||||
1. 继续按 `PR #307` 的 latest-head review 收尾,优先保持 active tracking 与 active trace 的单一锚点一致
|
||||
2. 若继续推进代码切片,先复核 request 侧是否仍存在与 stream invoker gate 对称的生成合同遗漏
|
||||
3. 进入下一批前继续使用最小 Release build 或 targeted test 作为权威验证,避免把环境噪音误判为代码问题
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
# Documentation Full Coverage Governance Status History (RP-049 to RP-052)
|
||||
|
||||
## Scope
|
||||
|
||||
- 该归档承接 active tracking 从 `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`
|
||||
|
||||
## 2026-04-29 / RP-049
|
||||
|
||||
- 重新进入时确认当前分支仍为 `docs/sdk-update-documentation`,但上游恢复路径不再依赖旧 PR 线程,而是改用 `origin/main` 作为 batch stop-condition baseline。
|
||||
- 本轮工作树在 reader-facing 文案收口后相对 `origin/main` 来到 `13` files / `132` lines,仍明显低于 `$gframework-batch-boot 50` 的阈值。
|
||||
- 主线程接受的变更范围限定在 `docs/zh-CN/godot/*`、`game/*` 和少量 README 标签,不扩展到导航重写或大型结构改稿。
|
||||
|
||||
## 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`。
|
||||
- 决策上只接受“改句子即可闭环”的问题,不把 README 子系统地图或结构级重写混入同一轮。
|
||||
- 当轮工作树相对 `origin/main` 为 `18` files / `225` lines,仍保留充足余量。
|
||||
|
||||
## 2026-04-29 / RP-051
|
||||
|
||||
- 从与 `origin/main` 零 diff 的状态重新进入后,把批次目标从“低风险句子收口”提升为“补新的 docs coverage 入口”。
|
||||
- 这一轮新增 `docs/zh-CN/source-generators/schema-config-generator.md`,并同步更新 `source-generators/index.md`、`api-reference/index.md`、`source-generators/cqrs-handler-registry-generator.md`、`core/cqrs.md` 与 `docs/.vitepress/config.mts`。
|
||||
- 接受的核心结论是:`Game.SourceGenerators` 需要新的 reader-facing 专题页,而 `SourceGenerators.Common` 和各 `*.SourceGenerators.Abstractions` 更适合作为现有入口中的共享排障层阅读路线。
|
||||
|
||||
## 2026-04-30 / RP-052
|
||||
|
||||
- 该批次提交为 `f88f96c3`(`docs(source-generators): 补充生成器专题覆盖并更新进度`)。
|
||||
- 提交后重新计算确认 committed branch diff vs `origin/main` 已回落到 `8` files / `337` lines,说明提交前的 `39` files / `2555` lines 只是临时工作树峰值,不应继续作为默认恢复指标。
|
||||
- active topic 后续仍可以继续按 `$gframework-batch-boot 50` 推进,但应优先挑“已有 package README、但站内专题仍不足”的覆盖切片。
|
||||
@ -0,0 +1,70 @@
|
||||
# Documentation Full Coverage Governance Validation History (RP-049 to RP-052)
|
||||
|
||||
## 2026-04-29 / RP-049
|
||||
|
||||
### 页面校验
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过;本轮页面的 frontmatter、链接与代码块校验通过。
|
||||
|
||||
### README 链接校验
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot/README.md tools/gframework-config-tool/README.md`
|
||||
- 结果:通过;reader-facing 链接标签调整后目标有效。
|
||||
|
||||
### 站点构建
|
||||
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
## 2026-04-29 / RP-050
|
||||
|
||||
### 页面校验
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||
- 结果:通过;本轮 3 个页面的 frontmatter、链接与代码块校验通过。
|
||||
|
||||
### README 链接校验
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Cqrs.Abstractions/README.md GFramework.SourceGenerators.Common/README.md`
|
||||
- 结果:通过;README reader-facing 标签调整后目标有效。
|
||||
|
||||
### 站点构建
|
||||
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮 reader-facing 收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
## 2026-04-30 / RP-051
|
||||
|
||||
### 页面校验
|
||||
|
||||
- `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/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||
- 结果:通过;新增专题页与相关入口页校验通过。
|
||||
|
||||
### 站点构建
|
||||
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;新增 `Schema 配置生成器` 入口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
## 2026-04-30 / RP-052
|
||||
|
||||
### 提交后状态确认
|
||||
|
||||
- `git status --short --branch`
|
||||
- `git diff --name-only origin/main...HEAD | wc -l`
|
||||
- `git diff --numstat origin/main...HEAD`
|
||||
- 结果:通过;提交后工作树 clean,相对 `origin/main` 的 committed diff 为 `8` files / `337` lines。
|
||||
@ -0,0 +1,31 @@
|
||||
# Documentation Full Coverage Governance Trace History (RP-049 to RP-052)
|
||||
|
||||
## Scope
|
||||
|
||||
- 该归档记录 `RP-049` 到 `RP-052` 从 active trace 迁出的阶段时间线,保留每轮恢复点、核心决策与停止条件。
|
||||
- 对应的验证明细继续保存在:
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-049-to-rp-052-2026-05-01.md`
|
||||
|
||||
## 2026-04-29 / RP-049
|
||||
|
||||
- 由于旧 PR 恢复路径已经失效,这一轮改用 `origin/main` 作为唯一 batch baseline,并重新确认当前分支仍为 `docs/sdk-update-documentation`。
|
||||
- 本轮主要清理 `godot`、`game` 与少量 README 中仍显露内部证据、命令式导流和原始路径标签的 reader-facing 问题。
|
||||
- 停止条件保持为 `$gframework-batch-boot 50`,但本轮并未逼近阈值。
|
||||
|
||||
## 2026-04-29 / RP-050
|
||||
|
||||
- 主线程继续沿低风险文案批次推进,接受 2 个 explorer 的热点排序,只处理“改句子即可闭环”的页面与 README 标签问题。
|
||||
- 实际落地集中在 `game/data.md`、`game/storage.md`、`godot/ui.md` 与 2 个 README。
|
||||
- 剩余命中已接近结构级 README 重写,因此被明确排除出这一轮批次。
|
||||
|
||||
## 2026-04-29 / RP-051
|
||||
|
||||
- 在确认 `HEAD` 与 `origin/main` 同步后,本轮把目标提升为新的 docs coverage 入口补链,而不是继续做纯措辞巡检。
|
||||
- 接受的结论是:`Game.SourceGenerators` 需要独立专题页,而 `SourceGenerators.Common` 与各 `*.SourceGenerators.Abstractions` 只需要在 landing / API 入口内承担共享 diagnostics 与 attribute 契约说明。
|
||||
- 与此同时,`Cqrs.SourceGenerators` 的真实缺口被限定为对 fallback 精度、分层策略和 `GF_Cqrs_001` 判断顺序的 reader-facing 解释,而不是继续新增第二张专题页。
|
||||
|
||||
## 2026-04-30 / RP-052
|
||||
|
||||
- 这一轮 coverage 扩展提交为 `f88f96c3`,提交后 branch diff 相对 `origin/main` 回落到 `8` files / `337` lines,重新释放了 stop-condition 余量。
|
||||
- active topic 后续仍可继续扩批,但更适合选择“已有 package README、但站内 docs 仍缺 reader-facing 专题”的切片,而不是继续给共享支撑层单开页面。
|
||||
- 该状态也为后续用 `$gframework-pr-review` 精确跟进最新 review 线程提供了更清晰的恢复入口。
|
||||
@ -2,75 +2,37 @@
|
||||
|
||||
## 目标
|
||||
|
||||
建立一个长期 active topic,持续治理 `GFramework` 的 README、`docs/zh-CN`、站点导航、XML 文档和 API
|
||||
参考链路,避免阶段性刷新完成后再次回漂。
|
||||
持续治理 `GFramework` 的 README、`docs/zh-CN`、站点导航、XML 文档和 API 参考链路,避免阶段性刷新完成后再次回漂。
|
||||
|
||||
- 用源码、测试、`*.csproj` 和必要的 `ai-libs/` 证据校正文档
|
||||
- 以模块族为单位闭环 README、landing page、专题页、教程入口和 API 参考链路
|
||||
- 明确哪些目录是可直接消费模块,哪些只是内部支撑模块
|
||||
- 把 XML 文档缺口纳入治理范围,而不是只刷新 Markdown
|
||||
- 把 XML 文档缺口与 reader-facing 采用路径持续纳入同一主题治理
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-050`
|
||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-055`
|
||||
- 当前阶段:`Phase 5 - Governance Maintenance`
|
||||
- 当前焦点:
|
||||
- 按 `$gframework-batch-boot 50` 继续推进 `documentation-full-coverage-governance`,沿用 `origin/main` 作为 stop-condition 基线,收口 `Game` / `Godot` 细页与少量 README 中残留的 reader-facing 措辞与标签问题
|
||||
- `2026-04-29` 重新进入时确认当前分支仍为 `docs/sdk-update-documentation`,但 upstream `origin/docs/sdk-update-documentation` 已不存在;因此本轮不再把旧 PR review 线程作为默认恢复入口,而是以本地 diff vs `origin/main` 为主
|
||||
- `2026-04-29` 上一批已完成 11 个低风险文档文件的收口:去掉 `ai-libs`、`旧文档`、`优先看` / `先看` / `转到` 这类内部或指令式措辞,并把 README 中暴露原始路径的链接标签改成 reader-facing 标题
|
||||
- `2026-04-29` 本批次继续接受 2 个 explorer 的只读结论:一个负责 `game/data.md`、`game/storage.md`、`godot/ui.md` 的热点排序,一个负责 README reader-facing 标签巡检;主线程只接受低风险措辞问题,不扩展到结构重写
|
||||
- `2026-04-29` 本批次已收口 `docs/zh-CN/game/data.md`、`game/storage.md`、`godot/ui.md` 与 `GFramework.Cqrs.Abstractions/README.md`、`GFramework.SourceGenerators.Common/README.md` 的文案:把内部证据叙述、外部项目指代、命令式导流、源文件路径列表和 `IsPackable=false` 这类实现术语改成 reader-facing 说明
|
||||
- 本轮仍保持在新的低风险批次窗口内:当前工作树相对 `origin/main` 为 `18` files / `225` lines,仍明显低于 `50` 文件 stop condition
|
||||
- 本轮继续沿用已确认的生命周期事实:`Architecture` 只暴露 `OnInitialize()`,`AbstractArchitecture` 通过 `InstallModules()` 暴露模块注册入口,而组件级 `OnInit()` 仍然是当前有效生命周期
|
||||
- 处理 PR `#308` 当前 latest-head review 与 outside-diff review 中经本地复核仍成立的 reader-facing 文档与 active `ai-plan` 漂移问题
|
||||
- 当前事实:
|
||||
- `2026-05-01` 重新抓取 `$gframework-pr-review` 后确认:PR `#308` 处于 `OPEN`,latest reviewed commit 已前进到 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117`
|
||||
- 当前 `CodeRabbit` 仍有 `5` 条 latest-head open threads;其中 `2` 条是本轮 latest review 针对 active tracking 与 `schema-config-generator.md` 措辞提出的新 actionable comments,另有 `3` 条为已被当前文件内容满足但尚未由远端重新计算关闭的 stale 线程
|
||||
- `Greptile` / `Gemini Code Assist` 当前无 open thread,CodeRabbit latest review 状态为 `CHANGES_REQUESTED`
|
||||
- 本地复核后,最新 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 元数据问题,不是仓库文件内可直接修复的阻塞项
|
||||
- 本地修复尚未推送前,不会改变远端 latest reviewed commit 与 open-thread 统计
|
||||
- 当前风险:
|
||||
- 如果 active tracking / trace 继续保留旧的 commit SHA 和 review 归因,会让后续恢复点重复处理已经本地闭环的问题
|
||||
- 在变更推送前,PR 页面仍会继续展示 latest reviewed commit `00ecf6fb...` 下的 open-thread 数量,容易把 stale 线程误判为新的本地缺陷
|
||||
|
||||
## 当前状态摘要
|
||||
|
||||
- `Core`、`Ecs.Arch`、`Cqrs`、`Game`、`Godot` 五个模块族当前都已有 README / landing / topic / API 参考层级的已验证入口。
|
||||
- `2026-04-29` 新一轮 batch boot 第 2 批次已进一步收口 `docs/zh-CN/game/data.md`、`game/storage.md`、`godot/ui.md` 与 `GFramework.Cqrs.Abstractions/README.md`、`GFramework.SourceGenerators.Common/README.md`:移除 “仓库和测试确认”“真实消费者 wiring”“CoreGrid 当前的做法”“优先看” 这类内部或生硬口吻,并把 CQRS 抽象层 README 的源文件列表改成契约类型族说明。
|
||||
- `2026-04-29` 新一轮 batch boot 已收口 `docs/zh-CN/godot/storage.md`、`godot/setting.md`、`godot/signal.md`、`godot/logging.md`、`godot/index.md`、`game/scene.md`、`core/index.md`、`game/config-system.md`、`ecs/arch.md` 与 `GFramework.Godot/README.md`、`tools/gframework-config-tool/README.md` 的 reader-facing 文案:移除 `ai-libs`、旧文档对比、命令式跳转和原始路径标签。
|
||||
- `2026-04-29` 当前分支的 upstream `origin/docs/sdk-update-documentation` 已 gone;后续若继续批处理,应继续以 `origin/main` 作为 branch-size stop condition 的 authoritative baseline,而不是默认恢复旧 PR review 状态。
|
||||
- `2026-04-28` 已重新抓取 PR `#299` 并复核 latest-head review:remote 当前只剩 `1` 条 `CodeRabbit` open thread 与 `1` 条 nitpick,且都指向 active tracking 文档;`Greptile` / `Gemini Code Assist` 当前无 open thread,测试汇总为 `2159 passed`,`Title check` 仍是 PR 元数据问题。
|
||||
- `2026-04-25` 已重新抓取 PR `#290` 并确认:latest reviewed commit 为 `54b8e5770af9ab3c8a86a396ffa4794fe4bb5181`,open thread 聚焦在 `docs/.vitepress/config.mts` 的侧栏重复 / 标签不一致,以及 `GFramework.Core`、`GFramework.Ecs.Arch`、`GFramework.Game` README 的 reader-facing 表格残留治理字段。
|
||||
- `2026-04-25` `docs/.vitepress/config.mts` 已保留 `source-generators` 栏目自有子页导航,但不再让 `api-reference` 侧栏重复跳回 `core`、`game`、`godot`、`ecs` 等独立栏目入口。
|
||||
- `2026-04-25` `GFramework.Core/README.md`、`GFramework.Ecs.Arch/README.md`、`GFramework.Game/README.md` 当前把 XML 阅读表统一收敛为“代表类型 + 阅读重点”,不再暴露日期、覆盖计数或 `已覆盖` 这类治理式字段。
|
||||
- `2026-04-25` `docs/zh-CN/contributing.md` 中最后一个嵌套 fenced 示例已改写为转义围栏文本,现有 `validate-code-blocks.sh` 不再报告第 `631` 行警告。
|
||||
- `2026-04-25` 全量 `docs/zh-CN` 验证已无剩余代码块语言警告;前一轮触达的 `tutorials`、`best-practices`、`troubleshooting`、`godot/resource` 等栏目结果保持有效。
|
||||
- `2026-04-25` `docs/zh-CN/source-generators/index.md` 已按 PR `#292` review 调整“共享支撑模块”段落句式,避免“对读者更重要的判断是”这类拗口表达。
|
||||
- `2026-04-25` `tools/gframework-config-tool/README.md` 已新增 `Documentation` 章节,直接链接到 `docs/zh-CN/game/config-tool.md` 与 `config-system.md`,让工具 README 能回到完整中文接入文档。
|
||||
- `2026-04-26` `tools/gframework-config-tool/README.md` 已补 `Quick Start`,把安装扩展、配置 `configPath` / `schemasPath`、打开 Explorer、先跑校验、再进入表单 / 批量编辑的最小接入路径串起来,并把 `Validation Coverage` 的 `stable config-schema subset` 统一为 `current schema subset`。
|
||||
- `2026-04-27` `docs/zh-CN/getting-started/installation.md` 已补齐当前公开选包矩阵,新增 `Core.Abstractions`、`Game.Abstractions`、`Ecs.Arch`、`Ecs.Arch.Abstractions` 的 reader-facing 安装说明,并把 `Godot` 常见问题里的旧版 `>= 4.5` 提示收敛到当前 `4.6.2` 基线。
|
||||
- `2026-04-27` `GFramework.Core.Abstractions/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md`、`GFramework.Ecs.Arch.Abstractions/README.md` 当前都已把 XML 阅读入口改写为“代表类型 + 阅读重点”,不再暴露覆盖计数、日期或 `已覆盖` 这类治理字段。
|
||||
- `2026-04-27` `docs/zh-CN/game/config-system.md` 与 `docs/zh-CN/tutorials/basic/index.md` 已把维护者 / 指挥式措辞改成中性的采用建议与阅读入口,避免公开页面继续暴露内部决策口吻。
|
||||
- `2026-04-27` `docs/zh-CN/getting-started/index.md`、`core/index.md`、`game/index.md`、`api-reference/index.md`、`source-generators/index.md` 已统一收敛为“适用场景 / 起步路线 / 继续阅读”式 reader-facing 入口,不再把 GitHub blob README 或治理说明当作主导航。
|
||||
- `2026-04-27` 新一轮 batch boot 第 1 批次已进一步收口 `docs/zh-CN/source-generators/index.md`、`game/index.md`、`api-reference/index.md`、`godot/setting.md`、`abstractions/index.md` 的标题与导航口吻,去掉 `family`、自我指涉标题、原始 `README.md` 文件名提示和“先理解…”式栏目标题。
|
||||
- `2026-04-27` 新一轮 batch boot 第 2 批次已把 `docs/zh-CN/game/ui.md`、`godot/signal.md`、`source-generators/godot-project-generator.md`、`get-node-generator.md`、`bind-node-signal-generator.md`、`auto-register-exported-collections-generator.md` 中直接暴露 `ai-libs/CoreGrid` 的路径型说明改成项目侧常见实现说明。
|
||||
- `2026-04-27` 新一轮 batch boot 第 3、4 批次已把 `core/query.md`、`core/command.md`、`core/context.md`、`core/lifecycle.md`、`game/scene.md`、`game/ui.md`、`godot/ui.md`、`godot/scene.md`、`source-generators/priority-generator.md`、`context-aware-generator.md` 中依赖“旧文档/旧入口”对比的句式改成直接陈述当前契约与推荐入口。
|
||||
- `2026-04-27` `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Godot/README.md`、`GFramework.Cqrs.Abstractions/README.md`、`GFramework.Ecs.Arch/README.md` 已收口 `ai-libs`、`family`、`seam`、`ReadMe.md` 等内部化或文件名式表述。
|
||||
- `2026-04-27` `docs/zh-CN` 当前已清空所有指向 `github.com/GeWuYou/GFramework/blob/main/.../README.md` 的公开外链,相关入口统一回到站内栏目页、专题页或 API 导航。
|
||||
- `2026-04-27` `docs/zh-CN/tutorials/godot-integration.md`、`game/setting.md`、`game/serialization.md`、`godot/index.md`、`godot/architecture.md`、`godot/storage.md`、`godot/logging.md`、`godot/setting.md`、`godot/extensions.md`、`core/architecture.md` 已把 `旧文档` / `ai-libs` / `.Wait()` / `family` 这类维护与内部语气改写成当前采用说明。
|
||||
- `2026-04-27` 已重新抓取 PR `#296` 并逐条复核 latest-head review:`GFramework.Game.SourceGenerators/README.md` 的 XML 阅读表已改成语义标签,`GFramework.Game/README.md` 已删除重复的 `storage.md` 入口,`docs/zh-CN/tutorials/godot-integration.md` 与 `docs/zh-CN/godot/extensions.md` 已收口仍成立的 reader-facing 措辞问题。
|
||||
- `2026-04-25` 当前批次已补齐 meta-package / 安装面:`GFramework.csproj` 不再保留占位描述,`README.md`、`docs/zh-CN/index.md`、`docs/zh-CN/getting-started/installation.md` 当前明确说明聚合元包只聚合 `Core` + `Game`,并把安装入口更新到当前 `net8.0/net9.0/net10.0` 与 Godot `4.6.2` 基线。
|
||||
- `2026-04-25` `docs/zh-CN/game/config-tool.md` 已新增为 reader-facing 工具页,`docs/zh-CN/game/index.md`、`config-system.md`、`docs/.vitepress/config.mts` 与 `tools/gframework-config-tool/README.md` 当前把 VS Code 配置工具纳入 `Game` 配置工作流入口。
|
||||
- `2026-04-25` source-generators 栏目已修正 4 处真实契约问题:`GetNode` 显式路径 / `Lookup` 语义、枚举生成器实际开关、`Context Get` 集合注入边界,以及 `GFramework.SourceGenerators.Common` / `*.SourceGenerators.Abstractions` 的共享支撑层说明。
|
||||
- `2026-04-25` `GFramework.SourceGenerators.Common/README.md`、`GFramework.Core.SourceGenerators.Abstractions/README.md`、`GFramework.Godot.SourceGenerators.Abstractions/README.md` 已补齐本地目录说明,根 README 的“内部支撑模块”表可以直接跳到对应目录说明。
|
||||
- `Game` persistence docs surface 当前以 `docs/zh-CN/game/data.md`、`storage.md`、`serialization.md`、`setting.md`
|
||||
作为最小巡检集合;若后续 README、runtime public API 或 `PersistenceTests` 变动,应优先复核这一组页面。
|
||||
- `Godot` runtime 与 generator 入口当前以 `GFramework.Godot/README.md`、
|
||||
`GFramework.Godot.SourceGenerators/README.md`、`docs/zh-CN/godot/index.md`、
|
||||
`docs/zh-CN/source-generators/index.md`、`docs/zh-CN/tutorials/godot-integration.md` 维持统一 owner / adoption path。
|
||||
- `2026-04-23` 到 `2026-04-24` 的批次细节、验证日志与旧恢复建议已迁入:
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-status-history-rp-023-to-rp-025-2026-04-24.md`
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 当前 `Core` / `Core.Abstractions`、`Ecs.Arch`、`Cqrs`、`Game` 的 XML 治理证据仍主要来自类型与入口级阅读,不等于成员级契约全审计;这类治理状态只应保留在 `ai-plan/**`,不应再回流到公开文档。
|
||||
- `GFramework.Cqrs` 在当前 WSL / dotnet 环境下仍会读取失效的 fallback package folder,并在标准 build 中触发
|
||||
`MSB4276` / `MSB4018`;这是已知环境阻塞,不属于本轮文档回归。
|
||||
- 当前 WSL 会话里 `git.exe` 可解析但不能执行,应继续使用显式 `--git-dir` / `--work-tree` 绑定作为默认 Git 策略。
|
||||
- `dotnet build GFramework.csproj -c Release` 当前仍会输出仓库既有 analyzer warnings(如 `MA0158`、`MA0051`、`MA0004`);本轮仅修改文档与 package metadata,不扩展到 warning 清理。
|
||||
- 当前分支 upstream 已 gone;在重新建立 remote branch 或新的 PR 之前,不适合再把旧 PR `#299` 的 review 状态当作默认恢复信号。
|
||||
- 当前 batch boot 已从 `origin/main` 零 diff 状态重新起步;本轮仍是低风险措辞收口,但下一轮若继续深入 README 子系统地图或大段采用路径重写,review 面会明显扩大。
|
||||
- `GFramework.Cqrs`、`GFramework.Cqrs.SourceGenerators`、`GFramework.Game.SourceGenerators`、`GFramework.Ecs.Arch` 等 README 仍有若干源文件列表式“子系统地图”段落;这些已经接近结构级改写,不适合作为本轮剩余低风险批次继续机械推进。
|
||||
- `source-generators` 栏目已经补出 `Schema 配置生成器` 专题页,并把 `Game.SourceGenerators` 接回 landing、API 入口与侧栏。
|
||||
- `Cqrs.SourceGenerators` 的 fallback 精度、`GF_Cqrs_001` 诊断边界与共享支撑层阅读路线,当前已经回收进现有专题页与入口页,而不是继续扩成新的维护者导向页面。
|
||||
- `RP-049` 到 `RP-052` 的阶段细节、逐命令验证和批次决策已迁入归档;active 文档只保留当前恢复事实、风险、验证结果与下一步。
|
||||
|
||||
## 归档指针
|
||||
|
||||
@ -80,59 +42,29 @@
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-status-history-through-rp-016.md`
|
||||
- 阶段状态归档(`RP-023` 到 `RP-025`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-status-history-rp-023-to-rp-025-2026-04-24.md`
|
||||
- 阶段状态归档(`RP-049` 到 `RP-052`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-status-history-rp-049-to-rp-052-2026-05-01.md`
|
||||
- 时间线归档(`RP-001` 到 `RP-016`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-through-rp-016.md`
|
||||
- 时间线归档(`RP-023` 到 `RP-025`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-023-to-rp-025-2026-04-24.md`
|
||||
- 时间线归档(`RP-041` 到 `RP-048`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- 时间线归档(`RP-049` 到 `RP-052`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-049-to-rp-052-2026-05-01.md`
|
||||
- 验证历史归档(`RP-041` 到 `RP-048`):
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- 验证历史归档(`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`
|
||||
|
||||
## 最新验证
|
||||
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`
|
||||
- 结果:通过;`game/data.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`
|
||||
- 结果:通过;`game/storage.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||
- 结果:通过;`godot/ui.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Cqrs.Abstractions/README.md GFramework.SourceGenerators.Common/README.md`
|
||||
- 结果:通过;本轮 2 个 README 的 reader-facing 标签调整后目标有效。
|
||||
- `2026-04-29` `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮第 2 批 reader-facing 收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
|
||||
- 结果:通过;`godot/storage.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
|
||||
- 结果:通过;`godot/setting.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||
- 结果:通过;`godot/signal.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||
- 结果:通过;`godot/logging.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||
- 结果:通过;`godot/index.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
|
||||
- 结果:通过;`game/scene.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
|
||||
- 结果:通过;`game/config-system.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- 结果:通过;`core/index.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过;`ecs/arch.md` 的 frontmatter、链接与代码块校验通过。
|
||||
- `2026-04-29` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot/README.md tools/gframework-config-tool/README.md`
|
||||
- 结果:通过;本轮 2 个 README 的 reader-facing 链接标签调整后目标有效。
|
||||
- `2026-04-29` `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮 reader-facing 收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
- `2026-04-28` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过;PR `#299` 处于 `OPEN`,latest head review 有 `1` 条 `CodeRabbit` open thread 与 `1` 条 nitpick,`Greptile` / `Gemini Code Assist` 当前无 open thread,测试汇总为 `2159 passed`,仅剩 `Title check` inconclusive。
|
||||
- `2026-04-28` `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;active tracking 收口与时间线归档瘦身后站点仍可构建,仅保留既有大 chunk warning。
|
||||
- `2026-04-27` 到 `2026-04-28` 的详细逐命令验证历史已迁入:
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- `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。
|
||||
- 本轮逐条命令与详细结果见 active trace 的 `RP-055` 验证段:
|
||||
- `ai-plan/public/documentation-full-coverage-governance/traces/documentation-full-coverage-governance-trace.md`
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 提交本轮第 2 批低风险 reader-facing 文案批次,并在提交后重新计算 branch diff vs `origin/main`,确认仍明显低于 `50` 文件 stop condition。
|
||||
2. 若继续下一批,优先挑选仍可“局部改句子”的页面或 README 标签问题;`Cqrs`、`Game.SourceGenerators`、`Ecs.Arch` 这类源文件列表式 README 应视作结构级专题,必要时单独开一轮更明确的写作目标。
|
||||
3. 只有在重新建立 remote branch 或新的 PR 之后,再恢复 `$gframework-pr-review` 作为默认恢复入口;在此之前以本地 diff 与验证结果为准。
|
||||
4. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API,优先复核 `docs/zh-CN/game/data.md`、`storage.md`、`serialization.md`、`setting.md` 与 landing page 是否仍保持同一套职责边界。
|
||||
1. 提交并推送本轮 source-generators 文档与 active `ai-plan` follow-up,然后重新抓取 `$gframework-pr-review`。
|
||||
2. 若 remote open threads 只剩 stale 的“迁移与兼容性”线程或 `Title check` 这类 metadata 项,则停止本轮仓库内修复。
|
||||
3. 若 review 线程清空或只剩 metadata 项,再按 `$gframework-batch-boot 50` 继续挑选新的 coverage 切片,避免在同一轮混入无关改稿。
|
||||
|
||||
@ -1,410 +1,53 @@
|
||||
# Documentation Full Coverage Governance Trace
|
||||
|
||||
## 2026-04-29
|
||||
## 2026-05-01
|
||||
|
||||
### 当前恢复点:RP-050
|
||||
### 当前恢复点:RP-055
|
||||
|
||||
- 本轮继续按 `$gframework-batch-boot 50` 推进,并沿用 `origin/main` `4557dde6`(`2026-04-29 11:14:56 +08:00`)作为唯一 branch-size baseline。
|
||||
- 当前 `HEAD` 相对 baseline 的 committed diff 仍是上一批的 `13` files / `133` lines;在本批次工作树修改与 `RP-050` 恢复文档更新后,working tree 相对 `origin/main` 为 `18` files / `225` lines,离 stop condition 仍有充足余量。
|
||||
- 本轮接受了 2 个 explorer 的只读排序:一个锁定 `docs/zh-CN/game/data.md`、`game/storage.md`、`godot/ui.md` 的低风险措辞问题,一个锁定 README 中仍能局部收口的标签问题。主线程只接受“改句子就能闭环”的项,不扩展到 README 结构重写。
|
||||
- 实际落地的收口集中在 5 个文件:`docs/zh-CN/game/data.md`、`game/storage.md`、`godot/ui.md`、`GFramework.Cqrs.Abstractions/README.md`、`GFramework.SourceGenerators.Common/README.md`。
|
||||
- 通过 `$gframework-pr-review` 重新抓取当前分支 PR `#308`,确认 latest reviewed commit 已前进到 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117`,当前 `CodeRabbit` 仍有 `5` 条 latest-head open threads。
|
||||
- 本地复核确认最新 review 中真正仍成立的 `2` 条问题已经在当前工作树完成修复:active tracking 的 RP-055 验证引用改为指向 active trace,以及 `schema-config-generator.md` 的“更稳妥地回退”措辞同步。
|
||||
- `schema-config-generator.md` 的“迁移与兼容性”章节与自包含运行时示例已在当前文件内容中满足 earlier open threads,对应 `3` 条历史线程现阶段应视为 stale,等待提交推送后由远端重新计算。
|
||||
- GitHub Test Reporter 当前汇总为 `2247 passed / 0 failed`;`Title check` 仍然只是 PR 元数据问题,因此不纳入仓库文件修复范围。
|
||||
|
||||
### 当前决策(RP-050)
|
||||
### 当前决策(RP-055)
|
||||
|
||||
- 文档页只处理内部证据口吻、命令式导流、外部项目指代和生硬 adoption phrasing;不改示例结构和导航层次。
|
||||
- README 只处理两类低风险项:把源文件路径列表改成类型级契约说明,把 `IsPackable=false` 这类实现术语改成 reader-facing 安装说明。
|
||||
- `GFramework.Cqrs`、`GFramework.Game.SourceGenerators`、`GFramework.Ecs.Arch` 等 README 的大段源文件清单继续留到后续单独批次,因为那已经接近结构级重写,不适合和当前轻量文案收口混在一轮。
|
||||
- 补齐 `cqrs-handler-registry-generator.md` 的 fallback 条件说明,以及 `schema-config-generator.md` 的自包含运行时示例。
|
||||
- active tracking 与 active trace 改写为与本次 PR 抓取一致的事实,修正 latest reviewed commit、线程数量、本地结论与验证摘要。
|
||||
- 只修仍然成立的 latest review 问题,不重复处理已被当前内容满足但尚未关闭的 stale 线程。
|
||||
- 本轮只做 PR review 精确收口,不扩展到新的 docs coverage 批次。
|
||||
|
||||
### 当前验证(RP-050)
|
||||
### 当前风险(RP-055)
|
||||
|
||||
- 页面校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||
- 结果:通过;本轮 3 个页面的 frontmatter、链接与代码块校验全部通过。
|
||||
- README 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Cqrs.Abstractions/README.md GFramework.SourceGenerators.Common/README.md`
|
||||
- 结果:通过;本轮 2 个 README 的 reader-facing 标签调整后目标有效。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮第 2 批 reader-facing 收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
- 在本轮提交推送前,PR 页面仍会继续展示 latest reviewed commit `00ecf6fb...` 下的 open threads,因此远端线程数量短时间内不会反映本地已修复状态。
|
||||
- `schema-config-generator.md` 的“迁移与兼容性”线程和 `configRootPath` 线程已经由当前文件内容满足,但仍需依赖远端下一次 review 重新计算后才能从 open thread 列表中消失。
|
||||
- `Title check` 仍是 PR 元数据层面的 `Inconclusive` 项;即使仓库文件全部修完,也不能仅凭本地改动让该检查转绿。
|
||||
|
||||
### 下一步(RP-050)
|
||||
|
||||
1. 提交本轮第 2 批 reader-facing 文案批次,并更新 committed branch diff vs `origin/main` 的精确计量。
|
||||
2. 若继续下一批,优先挑选仍可局部收口的页面或 README 标签,不把结构级 README 改写混入同一轮。
|
||||
3. 只有在 remote branch / 新 PR 重新建立后,再恢复 `$gframework-pr-review` 作为默认恢复入口。
|
||||
|
||||
### 当前恢复点:RP-049
|
||||
|
||||
- 本轮按 `$gframework-batch-boot 50` 恢复,继续沿用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`;当前 upstream `origin/docs/sdk-update-documentation` 已 gone,因此改用 `origin/main` `4557dde6`(`2026-04-29 11:14:56 +08:00`)作为新的 branch-size baseline。
|
||||
- 恢复时 committed branch diff vs baseline 为 `0` files / `0` lines,因此可以安全开启新一轮低风险 reader-facing 文档批次。
|
||||
- 当前工作树在本批次与恢复文档更新后相对 `origin/main` 为 `13` files / `132` lines,离 `$gframework-batch-boot 50` 的主 stop condition 仍有充足余量。
|
||||
- 本轮接受了 2 个 explorer 的只读热点排序:一个巡检 `docs/zh-CN/game` 与 `docs/zh-CN/godot` 细页,一个巡检模块 README 的 reader-facing 标签;主线程只接受低风险措辞问题,不扩展到 README 子系统地图或结构重写。
|
||||
- 实际落地的收口集中在 11 个文件:`docs/zh-CN/godot/storage.md`、`godot/setting.md`、`godot/signal.md`、`godot/logging.md`、`godot/index.md`、`game/scene.md`、`core/index.md`、`game/config-system.md`、`ecs/arch.md`、`GFramework.Godot/README.md`、`tools/gframework-config-tool/README.md`。
|
||||
|
||||
### 当前决策(RP-049)
|
||||
|
||||
- 由于 upstream / 旧 PR 恢复路径已经失效,本轮不再以旧的 PR `#299` review 线程作为批处理驱动条件,而是以 `origin/main` + `50` changed files 作为唯一 stop condition。
|
||||
- 这批修改只处理 reader-facing 措辞、交叉链接语气和 README 标签,不改导航结构、不补新章节、不重写示例。
|
||||
- explorer 给出的 `GFramework.Cqrs` / `GFramework.Cqrs.Abstractions` README 源文件列表问题先不纳入本轮,因为那已经超出“低风险文案收口”边界。
|
||||
|
||||
### 当前验证(RP-049)
|
||||
|
||||
- 页面校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过;本轮 9 个页面的 frontmatter、链接与代码块校验全部通过。
|
||||
- README 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot/README.md tools/gframework-config-tool/README.md`
|
||||
- 结果:通过;本轮 2 个 README 的 reader-facing 链接标签调整后目标有效。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮 reader-facing 收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
### 下一步(RP-049)
|
||||
|
||||
1. 提交本轮 reader-facing 文案批次,并更新 branch diff vs `origin/main` 的精确计量。
|
||||
2. 若继续下一批,优先复核 `docs/zh-CN/game/data.md`、`game/storage.md`、`godot/ui.md` 与少量 README 标签问题,不直接展开大段 README 子系统地图重写。
|
||||
3. 只有在 remote branch / 新 PR 重新建立后,再恢复 `$gframework-pr-review` 作为默认恢复入口。
|
||||
|
||||
## 2026-04-28
|
||||
|
||||
### 当前恢复点:RP-048
|
||||
|
||||
- 本轮按 `$gframework-pr-review` 抓取当前 PR `#299`,确认 latest head review 当前只剩 `1` 条 `CodeRabbit` open thread 与 `1` 条 nitpick;两者都指向 active tracking 文档本身,`Greptile` 与 `Gemini Code Assist` 当前无 open thread,测试汇总为 `2159 passed`,另有 `Title check` inconclusive。
|
||||
- 本地复核后确认:此前针对 `docs/zh-CN/abstractions/index.md`、`docs/zh-CN/core/lifecycle.md` 与相关教程 / 排障页的 review follow-up 已不再是当前 remote latest-head review 的剩余阻塞项。
|
||||
- 当前仍需收口的只剩两件事:为 `RP-048` 补齐明确的“下一步”段落,以及把 `RP-045` 到 `RP-047` 的逐命令时间线从 active trace 下沉到归档文件。
|
||||
|
||||
### 当前决策(RP-048)
|
||||
|
||||
- 本轮限定只修改 `ai-plan/public/documentation-full-coverage-governance` 下的 tracking / trace 文档,不再扩展到已经收口的公开文档页面。
|
||||
- active trace 只保留当前恢复点、验证结论、下一步与归档指针;`RP-041` 到 `RP-048` 的阶段细节转入专门的 trace archive,逐命令验证继续保留在 validation history archive。
|
||||
- 不把 `Title check` 当成仓库文件修复项;本轮完成后只需要在提交推送后重新抓取 PR review,确认 remote 线程状态是否清空。
|
||||
|
||||
### 当前验证(RP-048)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过;PR `#299` 处于 `OPEN`,latest head review 有 `1` 条 `CodeRabbit` open thread 与 `1` 条 nitpick,`Greptile` / `Gemini Code Assist` 当前无 open thread,测试汇总为 `2159 passed`,仅剩 `Title check` inconclusive。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;active tracking 收口与时间线归档瘦身后站点仍可构建,仅保留既有大 chunk warning。
|
||||
- 详细时间线归档:
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- 详细验证归档:
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
|
||||
### 下一步(RP-048)
|
||||
|
||||
1. 提交本轮 active tracking 收口改动,并将提交推送到 PR `#299`。
|
||||
2. 推送后重新抓取 `$gframework-pr-review`,确认 latest-head review 是否只剩 `Title check` 或已全部清空。
|
||||
3. 若仍有新的文档 review 线程,继续按 latest-head review 精确收口,不恢复关键词驱动的机械扩批。
|
||||
|
||||
## 2026-04-27
|
||||
|
||||
### 已归档历史(RP-041 到 RP-047)
|
||||
|
||||
- `RP-045` 到 `RP-047` 的 batch boot 逐阶段时间线、branch diff 计量与 review follow-up 决策,已迁入专门的 trace archive,避免 active trace 继续保留逐命令历史。
|
||||
- 对应的页面校验、README 链接校验与站点构建命令,继续保留在 validation history archive 中,供后续追溯。
|
||||
- 归档路径:
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
|
||||
### 当前恢复点:RP-044
|
||||
|
||||
- 本轮从 `$gframework-pr-review` 重新进入,继续沿用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`,并通过 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 抓取当前 PR `#296`。
|
||||
- 抓取结果显示 latest reviewed commit 为 `5778782df05e22dd24dc95189dd768458afb8537`,共有 `4` 条 open thread:`GFramework.Game.SourceGenerators/README.md` 的表头仍带路径视角、`GFramework.Game/README.md` 有重复 `storage.md` 链接、`docs/zh-CN/tutorials/godot-integration.md` 与 `docs/zh-CN/godot/extensions.md` 还有 reader-facing 措辞收口空间。
|
||||
- 本地逐条复核后确认这 `4` 条都仍成立,但都属于低风险文档收口;唯一 failed check `Title check` 只是 PR 标题元数据提示,不属于仓库文件内修复范围。
|
||||
|
||||
### 当前决策(RP-044)
|
||||
|
||||
- 接受 latest-head review 中仍成立的 `4` 条文档修正,不扩展到 review 未指向的其它页面,避免在当前接近 branch-size stop condition 的阶段继续增大 review 面。
|
||||
- 对 README 表格和导航问题,只做 reader-facing 命名与去重;对教程与 Godot 页面,只做措辞收口,不改变现有采用路径与示例结构。
|
||||
- 在同一轮里同步更新 active topic tracking / trace,并在提交前运行最小页面校验、README 链接校验与站点构建。
|
||||
|
||||
### 当前验证(RP-044)
|
||||
### 当前验证(RP-055)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
|
||||
- 结果:通过;PR `#296` 处于 `OPEN`,latest head review 共有 `4` 条 open thread,测试汇总为 `2156 passed`,仅剩 `Title check` inconclusive。
|
||||
- README 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game.SourceGenerators/README.md GFramework.Game/README.md`
|
||||
- 结果:通过;本轮 2 个 README 的 reader-facing 表格与导航去重调整后链接目标有效。
|
||||
- 结果:通过;PR `#308` 处于 `OPEN`,latest reviewed commit 为 `00ecf6fb1083e9039c9dc544a3265c38e1ba9117`,`CodeRabbit` 当前有 `5` 条 latest-head open threads,其中最新 review 的 `2` 条 actionable comments 已本地修复,其余 `3` 条应视为 stale;测试汇总为 `2247 passed / 0 failed`。
|
||||
- 页面校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||
- 结果:通过;两页 frontmatter、链接与代码块校验均通过。
|
||||
- `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/`)
|
||||
- 结果:通过;本轮 PR `#296` review 收口后的站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
### 当前恢复点:RP-043
|
||||
|
||||
- 在提交 `docs(reader-facing): 统一站内入口与公开术语` 后重新计算 branch diff,确认当前工作树继续补一批新文件后已到 `46` changed files,已经接近 `$gframework-batch-boot 50` 的停止线。
|
||||
- 因此本轮最后只接受 10 个还没进入 branch diff 的文件:`tutorials/godot-integration.md`、`game/setting.md`、`game/serialization.md`、`godot/index.md`、`godot/architecture.md`、`godot/storage.md`、`godot/logging.md`、`godot/setting.md`、`godot/extensions.md`、`core/architecture.md`。
|
||||
- 这批文件统一收口的是同一类问题:把 `旧文档`、`ai-libs`、`.Wait()`、`family` 之类维护 / 内部口吻改成当前采用指导,不扩新结构、不重写示例体系。
|
||||
|
||||
### 当前决策(RP-043)
|
||||
|
||||
- 当前 stop condition 已接近阈值,因此这批验证通过后立即停止继续扩批,避免 branch diff 超过 `50` files 或让 review 面退化。
|
||||
- 提交后本轮默认结束;后续若继续,应从 PR review 或剩余未触达的细页重新开一轮,而不是在同一轮里继续堆文件数。
|
||||
|
||||
### 当前验证(RP-043)
|
||||
|
||||
- 单页校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/architecture.md`
|
||||
- 结果:通过;本轮 10 个新文件的 frontmatter、链接与代码块校验全部通过。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;接近阈值前的最后一批文案收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
### 当前恢复点:RP-042
|
||||
|
||||
- 用户明确要求在当前阈值内循环推进,并允许使用 subagent 降低主线程上下文压力;因此本轮在主线程保留实现与验证,把热点识别委派给 3 个 explorer。
|
||||
- 接受的 subagent 结论主要有三类:
|
||||
- 入口页最划算的改法是统一 reader-facing 骨架,而不是继续保留治理说明或负向 framing。
|
||||
- 若站内已有栏目页与专题页,GitHub blob README 不应继续作为公开文档主导航。
|
||||
- `GFramework.Game` / `Game.Abstractions` / `Godot` 等 README 仍有 `ai-libs`、`family`、`seam`、`ReadMe.md` 等对外不友好的措辞,适合在同一轮里收口。
|
||||
- 基于这些结论,本轮连续落地 3 组低风险切片:入口页 reader-facing 改写、README / Godot 页去内部口吻、剩余 GitHub blob README 外链改回站内入口。
|
||||
|
||||
### 当前决策(RP-042)
|
||||
|
||||
- 继续保持 critical path 本地执行,不让 subagent 直接改文件;subagent 只负责热点排序与问题归类。
|
||||
- stop condition 继续沿用 `origin/main` + `50` changed files;当前工作树相对 baseline 的 tracked diff 已到 `36` files / `500` changed lines,意味着还能再做一小批,但应先提交当前稳定批次。
|
||||
- 当前批次不扩展到新栏目、新导航层或大段内容重写,只做 reader-facing 入口、术语和站内导航连通性收口。
|
||||
|
||||
### 当前验证(RP-042)
|
||||
|
||||
- README 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Godot/README.md GFramework.Cqrs.Abstractions/README.md GFramework.Ecs.Arch/README.md`
|
||||
- 结果:通过;本轮 5 个 README 的 reader-facing 改写后链接目标有效。
|
||||
- 教程 / Godot 页面校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||
- 结果:通过;受影响页面的 frontmatter、链接与代码块校验通过。
|
||||
- 入口与专题页校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started/quick-start.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/ui.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过;入口页和相关推荐入口改写后页面校验通过。
|
||||
- 栏目级校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators`
|
||||
- 结果:通过;抽象层与生成器栏目改回站内入口后栏目校验通过。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮 3 组 reader-facing 文档批次后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
### 当前恢复点:RP-041
|
||||
|
||||
- 通过 `$gframework-batch-boot 50` 重新进入后,先按仓库规则读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 与 active topic tracking / trace,并继续使用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`。
|
||||
- 使用显式 Git 绑定确认最新 baseline 为 `origin/main` `617e0bf`(`2026-04-26 12:17:15 +08:00`),当前 committed branch diff vs baseline 为 `0` files,因此本轮继续选择低风险、reader-facing 文档切片。
|
||||
- 本轮收敛出的 3 组切片分别是:`installation.md` 的选包矩阵与旧版 Godot 提示、公开 README 的 XML 阅读入口去治理化,以及 `config-system` / 基础教程入口中的维护者口吻改写。
|
||||
|
||||
### 当前决策(RP-041)
|
||||
|
||||
- 不扩展到导航结构或新专题页,只在现有入口上修正 reader-facing 采用路径与表述一致性。
|
||||
- 对公开 README 中的 XML 阅读入口,统一改成“代表类型 + 阅读重点”,不再暴露覆盖计数、日期或 `已覆盖` 这类治理字段。
|
||||
- stop condition 继续沿用 `origin/main` + `50` changed files;本轮工作树相对 baseline 的 tracked diff 为 `9` files / `191` changed lines,仍明显低于阈值。
|
||||
|
||||
### 当前验证(RP-041)
|
||||
|
||||
- README 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core.Abstractions/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch.Abstractions/README.md`
|
||||
- 结果:通过;本轮 4 个 README 的 reader-facing 改写后链接目标有效。
|
||||
- 入门栏目校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started`
|
||||
- 结果:通过;`installation.md` 更新后 `getting-started` 栏目 frontmatter、链接与代码块校验通过。
|
||||
- 配置系统页校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
|
||||
- 结果:通过;工具形态建议改写后页面校验通过。
|
||||
- 基础教程栏目校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
|
||||
- 结果:通过;入口页阅读路径改写后栏目校验通过。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;本轮文档批次后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
## 2026-04-26
|
||||
|
||||
### 当前恢复点:RP-040
|
||||
|
||||
- 本轮继续从 `$gframework-pr-review` 恢复,沿用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`,并重新抓取 PR `#292` 的 latest-head review。
|
||||
- 使用 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json` 抓取后确认:PR `#292` 最新 reviewed commit 为 `d3d62cf4541063c46458f88eea0f5acd1b4503f9`,failed checks 为 `0`,测试汇总仍为 `2156 passed`;剩余 `2` 条 CodeRabbit open thread 都落在 `tools/gframework-config-tool/README.md`。
|
||||
- 本地逐项复核后确认:缺少 `docs/zh-CN` 链接的评论已经过期,因为 README 当前已有 `Documentation` 章节;仍成立的是补最小接入路径,以及统一 `stable config-schema subset` / `current schema subset` 术语。
|
||||
|
||||
### 当前决策(RP-040)
|
||||
|
||||
- 接受当前 latest-head review 中仍然成立的两项 README 收口:新增 `Quick Start` 最小接入路径,并统一校验支持范围术语。
|
||||
- 不对已经过期的“缺少中文文档入口链接”线程做额外扩展,只在本地结果里保留“已验证为 stale”的结论,等待后续 PR review 刷新反映最新状态。
|
||||
- 继续遵守 active topic 的恢复要求,在同一轮里同步更新 tracking / trace,并对直接受影响的工具模块执行最小验证。
|
||||
|
||||
### 当前验证(RP-040)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||
- 结果:通过;PR `#292` 处于 `OPEN`,latest head review 还有 `2` 条 CodeRabbit open thread,failed checks 为 `0`,测试汇总为 `2156 passed`。
|
||||
- 工具 README 收口:
|
||||
- `tools/gframework-config-tool/README.md`
|
||||
- 结果:已新增 `Quick Start` 段落,并把 `Validation Coverage` 术语统一为 `current schema subset`。
|
||||
- 工具测试:
|
||||
- `bun run test`(工作目录:`tools/gframework-config-tool/`)
|
||||
- 结果:通过;`122` 个测试全部通过。
|
||||
- 工具打包:
|
||||
- `bun run package:vsix`(工作目录:`tools/gframework-config-tool/`)
|
||||
- 结果:通过;成功生成 `gframework-config-tool-0.0.3.vsix`,确认工具模块可完成最小打包验证。
|
||||
|
||||
## 2026-04-25
|
||||
|
||||
### 当前恢复点:RP-039
|
||||
|
||||
- 本轮从 `$gframework-pr-review` 重新进入,先按仓库规则读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 与 active topic tracking / trace,并继续使用显式 `--git-dir` / `--work-tree` 绑定确认当前分支为 `docs/sdk-update-documentation`。
|
||||
- 使用 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json` 抓取后确认:PR `#292` 最新 reviewed commit 为 `b96565ffa367bade30f44c2d4e8955143fbff85e`,latest head review 仅剩 `2` 条 CodeRabbit open thread,无 failed tests;唯一 failed check 为 `Title check` inconclusive,属于 PR 标题文案元数据提示。
|
||||
- 本地逐项复核后,两条 review 仍成立且都属于低风险 reader-facing 修正:
|
||||
- `docs/zh-CN/source-generators/index.md` 的“共享支撑模块”段落中,句式“对读者更重要的判断是”略拗口。
|
||||
- `tools/gframework-config-tool/README.md` 缺少通往 `docs/zh-CN/game/config-tool.md` 的中文接入文档入口。
|
||||
|
||||
### 当前决策(RP-039)
|
||||
|
||||
- 接受这两条 latest-head review,并限定本轮只做文案可读性与 README 入口补链,不扩展到未被当前 review 指向的其它页面。
|
||||
- `Title check` 不通过仓库文件修复;保持在本轮结果中显式记录,等待后续通过 GitHub PR 标题更新处理。
|
||||
- 继续沿用 active topic 的治理要求,在同一变更里同步更新 tracking / trace,保证后续从 PR review 恢复时能直接看到最新 commit 与剩余风险。
|
||||
|
||||
### 当前验证(RP-039)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||
- 结果:通过;PR `#292` 处于 `OPEN`,latest head review 还有 `2` 条 CodeRabbit open thread,测试汇总为 `2156 passed`,无 failed tests,另有 `Title check` inconclusive。
|
||||
|
||||
### 当前恢复点:RP-038
|
||||
|
||||
- 用户明确要求从“低效的单次批次”切到“循环跑到接近阈值”,并允许通过 subagent 避免主线程上下文过长;因此本轮把批处理目标从 PR `#290` 的单点收口扩展为“覆盖整个项目功能的 reader-facing 文档补齐”。
|
||||
- 先在主线程确认 critical path 仍是“选定低风险文档切片并控制 branch-size stop condition”,再委派 3 个 explorer 做只读巡检:
|
||||
- source-generator support modules / 文档失真点
|
||||
- CQRS 文档覆盖缺口
|
||||
- repo-root / tooling / meta-package surface
|
||||
- 接受的 explorer 结论:
|
||||
- `CQRS` 当前不需要扩独立栏目;最小有用修复是补 `docs/zh-CN/core/cqrs.md` 对 `RequestBase`、stream command/query 与协程入口的说明。
|
||||
- source-generators 当前最有价值的是修正文档失真,并补清楚 `GFramework.SourceGenerators.Common` 与 `*.SourceGenerators.Abstractions` 的共享支撑层语义。
|
||||
- repo-root / tooling 当前最缺的是 meta-package / install surface、VS Code config tool adoption path,以及 repo-visible support module README。
|
||||
- 由此收敛出 5 组连续低风险批次:
|
||||
- meta-package / 安装入口
|
||||
- config tool adoption
|
||||
- source-generators 真实契约修正
|
||||
- CQRS `Request` / stream 覆盖补齐
|
||||
- generator support module README
|
||||
|
||||
### 当前决策(RP-038)
|
||||
|
||||
- 不把 `CQRS` 从 `Core` 导航中抽成新栏目;本轮优先修正 reader-facing 覆盖缺口,而不是引入新的站点结构。
|
||||
- 对 repo-visible support modules,不扩成新的 docs 栏目,而是在各目录本地补 `README.md` 说明“为什么存在、跟谁一起走、什么时候需要读这里”。
|
||||
- 对 config tool,不新建顶级 `tooling/` 栏目,而是挂到 `Game` 下,保持它与 `config-system` 的采用路径一致。
|
||||
- stop condition 仍按 `origin/main` 与 `50` changed files 追踪;本轮提交前工作树已触达 `18` 个文件,仍明显低于阈值。
|
||||
|
||||
### 当前验证(RP-038)
|
||||
|
||||
- 文档栏目校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/index.md`
|
||||
- 结果:通过;触达页 frontmatter、链接与代码块校验通过。
|
||||
- README / 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh README.md tools/gframework-config-tool/README.md GFramework.SourceGenerators.Common/README.md GFramework.Core.SourceGenerators.Abstractions/README.md GFramework.Godot.SourceGenerators.Abstractions/README.md`
|
||||
- 结果:通过;根 README、config tool README 与新增 support README 的链接目标有效。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;站点可构建,仅保留既有大 chunk warning。
|
||||
- 元包编译:
|
||||
- `dotnet build GFramework.csproj -c Release`
|
||||
- 结果:通过;输出 `357` 条既有 analyzer warnings,无新增错误。
|
||||
|
||||
### 当前恢复点:RP-037
|
||||
|
||||
- 通过 `$gframework-batch-boot 50` 重新进入后,先按技能要求读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md`、active topic tracking / trace,并确认当前 worktree 仍映射到 `documentation-full-coverage-governance`。
|
||||
- 使用显式 `git --git-dir=<repo>/.git/worktrees/GFramework-update-documentation --work-tree=<worktree-root>` 绑定确认 baseline 采用 `origin/main` `79934f7`(`2026-04-25 16:15:55 +08:00`);branch diff vs baseline 当前为 `0` files,工作树仅有本批次改动。
|
||||
- 全量运行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN` 后确认 reader-facing 文档仅剩 `docs/zh-CN/contributing.md:631` 这一条既有代码块语言警告,适合作为单文件低风险批次收口。
|
||||
- 将 `docs/zh-CN/contributing.md` 的 Mermaid 示例从“真实嵌套 triple-backtick”改写为“外层 fenced block + 内层转义围栏文本”,避免当前 `validate-code-blocks.sh` 的简单 `^```` 状态机把内层 closing fence 误判成缺语言标记的新 opening fence。
|
||||
|
||||
### 当前决策(RP-037)
|
||||
|
||||
- 当前批处理目标收敛为“消除 `contributing.md` 中最后一个剩余代码块语言 warning”,不再继续扩展到别的栏目页。
|
||||
- 继续沿用 `origin/main` 作为 branch-size stop condition 基线,主指标仍是 `50` changed files;本批次只新增 1 个工作树文件,远未逼近阈值。
|
||||
- 对这类“文档中展示 Markdown 代码块”的示例,优先选择仓库现有校验脚本可稳定识别的转义文本写法,而不是依赖嵌套 fenced block 的解析细节。
|
||||
|
||||
### 当前验证(RP-037)
|
||||
|
||||
- 文档单文件校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/contributing.md`
|
||||
- 结果:通过;`docs/zh-CN/contributing.md` 不再报告第 `631` 行代码块语言警告。
|
||||
- 文档全量校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN`
|
||||
- 结果:通过;当前 `docs/zh-CN` 的 frontmatter、链接与代码块校验全部通过。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
### 当前恢复点:RP-036
|
||||
|
||||
- 本轮从 `$gframework-pr-review` 重新进入,目标不再是扩批,而是核对 PR `#290` latest-head review 仍未关闭的 reader-facing 文档问题。
|
||||
- 使用 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json` 抓取后确认:PR `#290` 最新 reviewed commit 为 `54b8e5770af9ab3c8a86a396ffa4794fe4bb5181`,CodeRabbit 与 Greptile 各有 `1` 条 open thread,失败检查为 `0`,测试汇总仍为 `2156 passed`。
|
||||
- 本轮把远端 review 与本地工作树逐项比对后,只接受仍然成立的 5 个 reader-facing 问题:`source-generators` 侧栏 3 个标签与目标标题不一致、`api-reference` 侧栏重复暴露跨栏目入口、`Core` / `Ecs.Arch` / `Game` README 仍保留 XML 覆盖基线字段。
|
||||
- 当前未提交批次限定在 `docs/.vitepress/config.mts`、3 个模块 README,以及 active tracking / trace;没有继续扩展到其他未被 review 指向的文档文件。
|
||||
|
||||
### 当前决策(RP-036)
|
||||
|
||||
- 对 PR review 的处理改成“只修当前 latest-head review 仍成立的问题”,不再延续前一轮的批量普查节奏。
|
||||
- `api-reference` 侧栏不再承载跨栏目目录跳转;跨模块导航继续保留在 `docs/zh-CN/api-reference/index.md` 的正文里,避免侧栏在跳出栏目后发生上下文切换。
|
||||
- `source-generators` 侧栏项统一与目标文档的 H1 / frontmatter `title` 对齐,避免同一页面在导航、标题与搜索索引里出现多套命名。
|
||||
- 模块 README 的 XML 阅读表只保留读者有用的“代表类型 / 阅读重点”,把覆盖计数、日期和 `已覆盖` 之类治理痕迹全部留在 `ai-plan/**`。
|
||||
|
||||
### 当前验证(RP-036)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||
- 结果:通过;PR `#290` 处于 `OPEN`,latest head review 还有 `2` 条 open thread,测试汇总为 `2156 passed`。
|
||||
|
||||
- README / 链接校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core/README.md GFramework.Ecs.Arch/README.md GFramework.Game/README.md`
|
||||
- 结果:通过;本轮 3 个 README 调整后链接目标仍然有效。
|
||||
- 站点构建:
|
||||
- `bun run build`(工作目录:`docs/`)
|
||||
- 结果:通过;`docs/.vitepress/config.mts` 的侧栏调整后站点仍可构建,仅保留既有大 chunk warning。
|
||||
- 结果:通过;source-generators 页面与 active `ai-plan` 更新后站点仍可构建,仅保留既有大 chunk warning。
|
||||
|
||||
### 归档指针
|
||||
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-status-history-through-rp-016.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-status-history-rp-023-to-rp-025-2026-04-24.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-through-rp-016.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-023-to-rp-025-2026-04-24.md`
|
||||
- `RP-041` 到 `RP-048` 的阶段时间线:
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- `RP-049` 到 `RP-052` 的阶段时间线:
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/traces/documentation-full-coverage-governance-trace-history-rp-049-to-rp-052-2026-05-01.md`
|
||||
- `RP-041` 到 `RP-048` 的验证明细:
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-rp-041-to-rp-048-2026-04-28.md`
|
||||
- `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`
|
||||
|
||||
### 下一步
|
||||
### 下一步(RP-055)
|
||||
|
||||
1. 完成 `bun run build` 与 README 链接校验后,提交当前 PR `#290` review 收口批次。
|
||||
2. 提交后再次运行 `$gframework-pr-review`,确认 CodeRabbit / Greptile 的 open thread 是否已关闭。
|
||||
3. 若仍有 review 残留,再按 latest-head review 精确收口,不恢复到前一轮的广覆盖批处理模式。
|
||||
1. 提交并推送本轮 follow-up 后重新抓取 `$gframework-pr-review`,确认 remote open threads 是否只剩 stale 的 schema 线程或 `Title check`。
|
||||
2. 若只剩 metadata 项,则把后续动作限定为 GitHub PR 元数据修正,不继续在仓库里做无关变更。
|
||||
3. 若 review 线程清空,则回到 `documentation-full-coverage-governance` 的下一个 coverage 切片,而不是继续在同一轮修改无关页面。
|
||||
|
||||
@ -248,6 +248,7 @@ export default defineConfig({
|
||||
text: '源码生成器',
|
||||
items: [
|
||||
{ text: '概览', link: '/zh-CN/source-generators/' },
|
||||
{ text: 'Schema 配置生成器', link: '/zh-CN/source-generators/schema-config-generator' },
|
||||
{ text: '日志生成器', link: '/zh-CN/source-generators/logging-generator' },
|
||||
{ text: '枚举扩展生成器', link: '/zh-CN/source-generators/enum-generator' },
|
||||
{ text: 'ContextAware 生成器', link: '/zh-CN/source-generators/context-aware-generator' },
|
||||
|
||||
@ -31,8 +31,8 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
||||
| 模块族 | 先看什么 | 继续深入 | XML 文档关注点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
|
||||
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract |
|
||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约;其中 AI-First 配置工作流的正式支持边界以 Runtime + Generator 共享 schema 子集为准 |
|
||||
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract,以及生成注册器与定向补扫的协作边界 |
|
||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md)、[Schema 配置生成器](../source-generators/schema-config-generator.md) | 配置、数据、设置、场景、UI、存储、序列化契约,以及 schema 到生成代码的公开入口 |
|
||||
| `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
|
||||
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
|
||||
|
||||
@ -78,6 +78,22 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
- 主要提供共享生成器基类、通用 diagnostics,以及生成方法冲突等跨模块约束。
|
||||
|
||||
如果你要沿着 XML 文档和源码继续读,优先从下面这几类入口开始:
|
||||
|
||||
- 共享 diagnostics
|
||||
- `CommonDiagnostics`
|
||||
- 共享生成流程
|
||||
- `AttributeClassGeneratorBase`
|
||||
- `AttributeEnumGeneratorBase`
|
||||
- 共享冲突规则
|
||||
- `GeneratedMethodConflictExtensions`
|
||||
|
||||
这组入口更适合回答三类问题:
|
||||
|
||||
- 为什么多个生成器都会要求宿主类型满足 `partial`
|
||||
- 为什么不同专题页会出现同一套生成方法名冲突诊断
|
||||
- 为什么多个生成器对候选筛选、属性解析和生成阶段采用相近流程
|
||||
|
||||
## 使用方式
|
||||
|
||||
把本页当成“API 阅读导航”而不是“签名快照”:
|
||||
|
||||
@ -165,14 +165,30 @@ protected override void OnInitialize()
|
||||
|
||||
1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute`
|
||||
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
|
||||
3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径
|
||||
4. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
|
||||
5. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
|
||||
6. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
|
||||
7. 同一程序集按稳定键去重,避免重复注册
|
||||
3. 当生成注册器同时暴露 generated request invoker provider 时,runtime 会把 request/response 类型对对应的 descriptor 预先接线到 dispatcher 缓存,后续请求分发优先消费这些 generated request invoker 元数据
|
||||
4. 当生成注册器同时暴露 generated stream invoker provider 时,runtime 会以同样方式优先消费 stream request 对应的 generated stream invoker descriptor;只有当前类型对未命中时,才回退到既有反射 stream binding
|
||||
5. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射绑定,已命中的不兼容元数据会直接抛出异常
|
||||
6. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
|
||||
7. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]` 和 `string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
|
||||
8. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
|
||||
9. 同一程序集按稳定键去重,避免重复注册
|
||||
|
||||
换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是:生成注册器负责能静态表达的部分,fallback 只补它覆盖不到的 handler。
|
||||
|
||||
如果你在阅读 dispatcher 行为,可以把这部分理解成两组并列能力:
|
||||
|
||||
- request invoker provider / descriptor
|
||||
- 面向 `SendRequestAsync(...)`、`SendAsync(...)`、`SendQueryAsync(...)` 这类单次请求分发
|
||||
- stream invoker provider / descriptor
|
||||
- 面向 `CreateStream(...)` 触发的流式请求分发
|
||||
|
||||
两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。
|
||||
|
||||
对接入方来说,更关键的 reader-facing 语义是:安装 `Cqrs.SourceGenerators` 后,不要求“所有 handler 都能被生成代码直接引用”才有收益。
|
||||
即使仍有 fallback,runtime 也会先消费 generated registry,再只对剩余 handler 做定向补扫;只有旧版 marker 语义或空 fallback 元数据才会退回整程序集扫描。
|
||||
`Type` fallback、按名称恢复的 fallback,以及 mixed fallback 只影响补扫精度,不改变
|
||||
`RegisterCqrsHandlersFromAssembly(...)` 或 `RegisterCqrsHandlersFromAssemblies(...)` 的接法。
|
||||
|
||||
`Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。
|
||||
|
||||
## Pipeline Behavior
|
||||
@ -225,7 +241,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
||||
| `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime`、`ICqrsHandlerRegistrar`、`IPipelineBehavior<,>`、`IRequestHandler<,>`、`Unit` | 请求、处理器和 runtime seam 的最小契约 |
|
||||
| `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase<TInput, TResponse>`、`QueryBase<TInput, TResponse>`、`NotificationBase<TInput>`、`RequestBase<TInput, TResponse>`、`ContextAwareCqrsExtensions` | 业务侧常用基类和上下文发送入口 |
|
||||
| `GFramework.Cqrs/Cqrs/` | `AbstractCommandHandler<,>`、`AbstractQueryHandler<,>`、`AbstractRequestHandler<,>`、`AbstractStreamCommandHandler<,>`、`AbstractStreamQueryHandler<,>`、`LoggingBehavior<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 |
|
||||
| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`DefaultCqrsRegistrationService` | runtime 创建入口、generated-registry 优先级、targeted fallback 语义和程序集去重规则 |
|
||||
| 运行时入口与内部协作层 | `CqrsRuntimeFactory`、`ICqrsHandlerRegistry`、`CqrsHandlerRegistryAttribute`、`CqrsReflectionFallbackAttribute`、`ICqrsRequestInvokerProvider`、`ICqrsStreamInvokerProvider` | runtime 创建入口、generated-registry 优先级、request / stream invoker provider 协作点、targeted fallback 语义和程序集去重规则 |
|
||||
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator`、`RuntimeTypeReferenceSpec`、`OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 |
|
||||
|
||||
## 继续阅读
|
||||
|
||||
@ -6,7 +6,8 @@ description: 为消费端程序集生成 CQRS handler registry,并在需要时
|
||||
# CQRS Handler Registry 生成器
|
||||
|
||||
`GFramework.Cqrs.SourceGenerators` 会在编译期为当前业务程序集生成 `ICqrsHandlerRegistry`,让 `GFramework.Cqrs`
|
||||
runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整个程序集。
|
||||
runtime 在注册 handlers 时优先走静态注册表;当运行时合同允许时,也会把 request / stream 分发可直接复用的 invoker
|
||||
元数据前移到编译期,而不是总是先扫描整个程序集或在首次分发时再走反射绑定。
|
||||
|
||||
它服务的是 `Cqrs` 家族,不是独立运行时:
|
||||
|
||||
@ -27,11 +28,17 @@ runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整
|
||||
1. 一个实现 `ICqrsHandlerRegistry` 的内部注册器类型
|
||||
2. 程序集级 `CqrsHandlerRegistryAttribute`
|
||||
|
||||
当运行时暴露对应合同、且当前 handler 可被安全静态表达时,生成注册器还可以继续暴露:
|
||||
|
||||
- generated request invoker provider / descriptor
|
||||
- generated stream invoker provider / descriptor
|
||||
|
||||
当某些 handler 不能被生成代码安全地直接引用时,还会补发:
|
||||
|
||||
- 程序集级 `CqrsReflectionFallbackAttribute`
|
||||
|
||||
这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。
|
||||
这意味着运行时会先使用生成注册器完成可静态表达的映射;对 request 与 stream 分发来说,也会优先消费 generated invoker
|
||||
descriptor。只有当前类型对没有 generated metadata,或 registry / fallback 无法覆盖时,才继续回到既有反射 binding 或补扫路径,而不是退回整程序集盲扫。
|
||||
如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时,mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。
|
||||
|
||||
## 最小接入路径
|
||||
@ -78,12 +85,17 @@ RegisterCqrsHandlersFromAssemblies(
|
||||
|
||||
1. 先读取程序集上的 `CqrsHandlerRegistryAttribute`
|
||||
2. 优先激活生成的 `ICqrsHandlerRegistry`
|
||||
3. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
|
||||
4. 若存在 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
|
||||
5. 同一程序集按稳定键去重,避免重复注册
|
||||
3. 若生成注册器同时提供 request invoker provider / descriptor,registrar 会把这些 request invoker 元数据预先登记到 dispatcher 缓存
|
||||
4. 若生成注册器同时提供 stream invoker provider / descriptor,runtime 也会优先消费对应的 generated stream invoker 元数据;未命中时仍回退到既有反射 stream binding
|
||||
5. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
|
||||
6. 若存在 `CqrsReflectionFallbackAttribute`,优先按其中携带的 `Type` 或类型名补扫剩余 handler;若元数据为空或只保留 marker 语义,则退回整程序集补扫
|
||||
7. 同一程序集按稳定键去重,避免重复注册
|
||||
|
||||
这个行为由 `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 和
|
||||
`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 共同覆盖。
|
||||
这个行为由
|
||||
[运行时注册流程测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs)
|
||||
和
|
||||
[生成器行为测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs)
|
||||
共同覆盖。
|
||||
|
||||
## 什么时候值得安装
|
||||
|
||||
@ -91,6 +103,7 @@ RegisterCqrsHandlersFromAssemblies(
|
||||
|
||||
- 业务程序集内 handler 数量较多
|
||||
- 想把 handler 注册路径前移到编译期
|
||||
- 想把 request / stream 分发里可静态确定的 invoker metadata 一并前移到编译期
|
||||
- 希望冷启动阶段减少整程序集反射扫描
|
||||
- 需要更明确地观察“哪些 handler 走静态注册,哪些只能走 fallback”
|
||||
|
||||
@ -114,11 +127,46 @@ RegisterCqrsHandlersFromAssemblies(
|
||||
- 其余场景统一回退到字符串元数据,避免 mixed 场景漏注册
|
||||
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
|
||||
|
||||
## 生成策略层级
|
||||
|
||||
把这个生成器理解成“静态注册 or 整程序集扫描”的二选一,会低估它的收益。当前策略实际上分成四层:
|
||||
|
||||
1. 直接静态注册
|
||||
- handler 接口和实现类型都能被生成代码安全引用
|
||||
2. 实现类型定向反射查找
|
||||
- handler 接口还能精确表达,但实现类型只能在运行时按具体类型名恢复
|
||||
3. service type 精确运行时查找
|
||||
- handler 接口本身也需要运行时构造,但仍能把查找范围收窄到具体 service type
|
||||
4. 程序集级 fallback 元数据
|
||||
- 只有前面几层都无法覆盖的剩余 handler,才交给 `CqrsReflectionFallbackAttribute`
|
||||
|
||||
这意味着安装生成器后,并不要求“所有 handler 都可直接引用”才有收益。很多只能部分静态表达的项目,仍然可以把大部分注册路径前移到编译期,再对少数复杂类型做定向补扫。
|
||||
|
||||
## 哪些场景通常不会直接退回整程序集扫描
|
||||
|
||||
下列类型形态经常仍然能保留精细化注册,而不是立刻退回整程序集盲扫:
|
||||
|
||||
| 场景 | 常见结果 |
|
||||
| --- | --- |
|
||||
| 私有嵌套 handler,但对外 handler 接口仍可直接引用 | 生成器改为按实现类型定向反射查找 |
|
||||
| 响应或参数里包含需要运行时恢复的隐藏类型 | 生成器改为精确 service type runtime lookup |
|
||||
| mixed 场景里同时存在可直接引用和仅能按名称恢复的 fallback handlers | 生成器拆分 `Type` 元数据和字符串元数据,减少后续字符串回查 |
|
||||
| 响应类型写成 `dynamic` | 生成器会按 `System.Object` 归一化,而不是发射非法的 `typeof(dynamic)` |
|
||||
|
||||
相反,pointer、function-pointer 这类无法安全重建的类型形态,不属于这里承诺的精确生成边界。
|
||||
|
||||
如果当前编译环境缺少这个 fallback 合同,而某些 handler 又必须依赖它,生成器会报:
|
||||
|
||||
- `GF_Cqrs_001`
|
||||
|
||||
这条诊断的含义不是“某个 handler 写错了”,而是“当前 runtime 合同不足以安全承载这轮生成结果”。
|
||||
遇到它时,优先按这个顺序判断:
|
||||
|
||||
1. 当前消费端是否已经引用支持 `CqrsReflectionFallbackAttribute` 的 `GFramework.Cqrs` runtime
|
||||
2. 当前项目里是否存在只能部分静态表达的 handler 类型
|
||||
3. 如果确实不想引入 fallback 合同,是否需要把这类 handler 改成更容易被生成器直接引用的公开形态
|
||||
|
||||
`CqrsReflectionFallbackAttribute` 出现也不等于“运行时一定回到整程序集扫描”。只有 fallback 元数据为空、或旧版只保留 marker 语义时,runtime 才会退回整程序集补扫;当元数据里已经带了具体 `Type` 或类型名时,runtime 会优先按这些剩余 handler 做定向补扫。
|
||||
|
||||
## 源码与 API 阅读入口
|
||||
|
||||
@ -127,6 +175,8 @@ RegisterCqrsHandlersFromAssemblies(
|
||||
- `GFramework.Cqrs.ICqrsHandlerRegistry`
|
||||
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
|
||||
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
|
||||
- `GFramework.Cqrs.ICqrsRequestInvokerProvider`
|
||||
- `GFramework.Cqrs.ICqrsStreamInvokerProvider`
|
||||
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
|
||||
|
||||
模块族入口见:
|
||||
|
||||
@ -25,7 +25,7 @@ GFramework 当前发布的生成器包是:
|
||||
| 使用场景 | 安装包 | 继续阅读 |
|
||||
| --- | --- | --- |
|
||||
| 减少日志、上下文注入、模块自动注册等 Core 侧样板代码 | `GeWuYou.GFramework.Core.SourceGenerators` | [Core 模块](../core/index.md)、[日志生成器](./logging-generator.md)、[ContextAware 生成器](./context-aware-generator.md) |
|
||||
| 把 `schemas/**/*.schema.json` 生成成配置类型和表包装 | `GeWuYou.GFramework.Game.SourceGenerators` | [配置系统](../game/config-system.md)、[VS Code 配置工具](../game/config-tool.md) |
|
||||
| 把 `schemas/**/*.schema.json` 生成成配置类型和表包装 | `GeWuYou.GFramework.Game.SourceGenerators` | [Schema 配置生成器](./schema-config-generator.md)、[配置系统](../game/config-system.md)、[VS Code 配置工具](../game/config-tool.md) |
|
||||
| 让 CQRS handler registry 在编译期生成,缩小运行时反射扫描范围 | `GeWuYou.GFramework.Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md)、[CQRS Handler Registry 生成器](./cqrs-handler-registry-generator.md) |
|
||||
| 在 Godot 项目里生成 AutoLoad / Input Action 入口、节点 / 信号样板,或补齐 Scene/UI 包装与导出集合注册辅助 | `GeWuYou.GFramework.Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md)、[Godot 项目生成器](./godot-project-generator.md)、[GetNode 生成器](./get-node-generator.md) |
|
||||
|
||||
@ -94,6 +94,15 @@ GFramework 当前发布的生成器包是:
|
||||
- 再根据 attribute 或 diagnostics 回到对应专题页
|
||||
- 只有在排查生成失败原因时,才继续下钻到这些共享支撑目录
|
||||
|
||||
如果你更关心“多个生成器为什么会给出一致的 `partial` 要求、方法名冲突错误或 trace 诊断”,可以把这三类目录当成共享排障层来理解:
|
||||
|
||||
- `*.SourceGenerators.Abstractions`
|
||||
- 先看公开 attribute 能写什么,再回到对应专题页确认生成语义
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
- 先看共享 diagnostics,再看公共生成基类和冲突规则
|
||||
|
||||
这层仍然不是新的安装入口。只有在排查生成失败、对比多个生成器的共同约束,或准备扩展生成器时,才值得继续下钻。
|
||||
|
||||
## 阅读路线
|
||||
|
||||
### Core 侧通用生成器
|
||||
@ -107,6 +116,8 @@ GFramework 当前发布的生成器包是:
|
||||
|
||||
### Game / CQRS 相关生成器
|
||||
|
||||
- schema 到配置类型 / 表包装 / 聚合注册的生成路径:
|
||||
- [Schema 配置生成器](./schema-config-generator.md)
|
||||
- 配置 schema 生成与运行时接法:
|
||||
- [配置系统](../game/config-system.md)
|
||||
- 读者若需要确认共享 schema 子集、关闭对象边界或复杂组合关键字的限制,应以该页为准,而不是只从本页推断支持范围
|
||||
@ -138,6 +149,8 @@ GFramework 当前发布的生成器包是:
|
||||
- 安装 `Game` + `Game.SourceGenerators`
|
||||
- 需要 CQRS 生成注册表:
|
||||
- 安装 `Cqrs` + `Cqrs.SourceGenerators`
|
||||
- 需要排查跨生成器共享的 diagnostics 或 attribute 契约:
|
||||
- 保持原有 `*.SourceGenerators` 包入口不变,再回到本页的共享支撑模块说明
|
||||
|
||||
## 对应模块入口
|
||||
|
||||
|
||||
227
docs/zh-CN/source-generators/schema-config-generator.md
Normal file
227
docs/zh-CN/source-generators/schema-config-generator.md
Normal file
@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Schema 配置生成器
|
||||
description: 说明 GFramework.Game.SourceGenerators 如何从 schema 生成配置类型、表包装和聚合注册入口。
|
||||
---
|
||||
|
||||
# Schema 配置生成器
|
||||
|
||||
`GFramework.Game.SourceGenerators` 会把消费者项目里的 `schemas/**/*.schema.json` 读入编译期管线,并生成:
|
||||
|
||||
- 强类型配置类型
|
||||
- 对应的表包装类型
|
||||
- 单表绑定辅助代码
|
||||
- 聚合注册目录与 `RegisterAllGeneratedConfigTables(...)` 扩展入口
|
||||
|
||||
如果你当前目标是“先把配置系统接进项目”,先看 [游戏内容配置系统](../game/config-system.md)。这页更适合在你已经决定使用
|
||||
`Game.SourceGenerators` 之后,继续确认 schema 输入契约、生成结果和常见诊断边界。
|
||||
|
||||
## 它解决什么问题
|
||||
|
||||
相比只写 `YAML` 和运行时加载代码,这个生成器把三件事前移到了编译期:
|
||||
|
||||
- 把 schema 转成 `Config` 类型,让业务代码直接拿到强类型字段和 XML 文档
|
||||
- 为运行时表生成包装层,让 `Get`、`TryGet`、按字段查找等入口保持稳定
|
||||
- 汇总当前项目中所有 schema 的注册信息,避免 schema 数量增长后继续手写逐表注册
|
||||
|
||||
这也是 `GFramework.Game` 配置运行时、VS Code 配置工具和 schema 约束能够共享同一份结构定义的基础。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
NuGet 安装保持运行时包与生成器包版本一致,并把生成器声明为编译期依赖:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GeWuYou.GFramework.Game" Version="x.y.z" />
|
||||
<PackageReference Include="GeWuYou.GFramework.Game.SourceGenerators"
|
||||
Version="x.y.z"
|
||||
PrivateAssets="all"
|
||||
ExcludeAssets="runtime" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
默认情况下,包内 `targets` 会自动把 `schemas/**/*.schema.json` 纳入 `AdditionalFiles`。如果你的 schema 目录不是
|
||||
`schemas/`,可以在项目文件里覆盖:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GFrameworkConfigSchemaDirectory>GameSchemas</GFrameworkConfigSchemaDirectory>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
## 输入约定
|
||||
|
||||
### schema 根对象
|
||||
|
||||
当前生成器要求每个 schema 都满足这些基本约束:
|
||||
|
||||
- 顶层 `type` 必须是 `object`
|
||||
- 必须声明必填 `id` 字段
|
||||
- `id` 目前只支持 `integer` 和 `string`
|
||||
- schema 文件名与属性名都必须能稳定映射到合法的 C# 标识符
|
||||
|
||||
最小示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monster Config",
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 路径与索引元数据
|
||||
|
||||
除了标准 JSON Schema 字段,当前还支持几个会直接影响生成结果的扩展元数据:
|
||||
|
||||
- `x-gframework-config-path`
|
||||
- 覆盖默认配置目录;值必须是相对路径,且不能包含 `.`、`..` 或绝对路径段
|
||||
- `x-gframework-index`
|
||||
- 为顶层必填、非主键、非引用的标量字段生成 `FindBy...` / `TryFindFirstBy...` 查找入口
|
||||
- `x-gframework-ref-table`
|
||||
- 为字段补充跨表引用语义,并把这部分信息写入生成的绑定元数据
|
||||
|
||||
如果某个字段不满足 lookup index 的安全条件,生成器会直接报诊断,而不是静默生成一个容易失真的查询入口。
|
||||
|
||||
### 当前稳定约束子集
|
||||
|
||||
从源码和快照测试看,当前共享子集已经覆盖:
|
||||
|
||||
- 标量、嵌套对象、对象数组、标量数组
|
||||
- `default`、`enum`、`const`
|
||||
- `minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`
|
||||
- `minLength`、`maxLength`、`pattern`
|
||||
- `format`
|
||||
- 当前稳定字符串子集是 `date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`
|
||||
- `minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`
|
||||
- `minProperties`、`maxProperties`
|
||||
- `dependentRequired`、`dependentSchemas`、`allOf`
|
||||
- 面向对象约束的 `if` / `then` / `else`
|
||||
|
||||
这些约束并不一定都会改变生成类型的形状,但会被保留到生成代码文档和绑定元数据里,方便运行时与工具链共享。
|
||||
|
||||
## 会生成什么
|
||||
|
||||
以 `monster.schema.json` 为例,当前生成器会形成四组稳定输出:
|
||||
|
||||
### 1. 配置类型
|
||||
|
||||
例如 `MonsterConfig`。它承载 schema 字段到 C# 属性的映射,以及对应的 XML 文档。
|
||||
|
||||
### 2. 表包装类型
|
||||
|
||||
例如 `MonsterTable`。它包装运行时 `IConfigTable<TKey, TValue>`,并在需要时提供生成的查找入口:
|
||||
|
||||
```csharp
|
||||
public sealed partial class MonsterTable : IConfigTable<int, MonsterConfig>
|
||||
{
|
||||
public MonsterConfig Get(int key) { ... }
|
||||
|
||||
public IReadOnlyList<MonsterConfig> FindByName(string value) { ... }
|
||||
|
||||
public bool TryFindFirstByName(string value, out MonsterConfig? result) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
如果字段声明了 `x-gframework-index`,生成器会优先使用延迟构建的只读索引;否则按当前表快照做确定性的线性扫描,以保持和热重载后的运行时数据一致。
|
||||
|
||||
### 3. 单表绑定辅助
|
||||
|
||||
例如 `MonsterConfigBindings`。这里会保留单表注册所需的表名、schema 路径、配置路径和引用元数据,方便项目侧继续组合自己的启动逻辑。
|
||||
|
||||
### 4. 聚合注册目录
|
||||
|
||||
当一个项目里有多个 schema 时,生成器还会汇总出 `GeneratedConfigCatalog` 与聚合扩展:
|
||||
|
||||
```csharp
|
||||
loader.RegisterAllGeneratedConfigTables();
|
||||
```
|
||||
|
||||
如果你需要按表传入额外比较器或做更细粒度控制,还可以使用带 `GeneratedConfigRegistrationOptions` 的重载。
|
||||
|
||||
## 运行时如何消费这些输出
|
||||
|
||||
最常见的消费路径是把生成器输出交给 `GameConfigBootstrap` 或 `YamlConfigLoader`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Config;
|
||||
using GFramework.Game.Config.Generated;
|
||||
|
||||
var configRootPath = Path.Combine(AppContext.BaseDirectory, "schemas");
|
||||
|
||||
var bootstrap = new GameConfigBootstrap(
|
||||
new GameConfigBootstrapOptions
|
||||
{
|
||||
RootPath = configRootPath,
|
||||
ConfigureLoader = static loader => loader.RegisterAllGeneratedConfigTables()
|
||||
});
|
||||
```
|
||||
|
||||
当你需要继续手动拼装运行时,也可以直接用单表绑定或聚合目录做自己的注册封装。这时建议把“启动生命周期”和“业务读取入口”分开,继续沿用
|
||||
[游戏内容配置系统](../game/config-system.md) 里的 `GameConfigHost` / `GameConfigRuntime` 模板。
|
||||
|
||||
## 迁移与兼容性
|
||||
|
||||
如果你当前项目还在手写逐表注册,最小迁移路径可以按下面的顺序推进:
|
||||
|
||||
1. 保持 `GeWuYou.GFramework.Game` 与 `GeWuYou.GFramework.Game.SourceGenerators` 版本一致,并把生成器包声明为 `PrivateAssets="all"` 的编译期依赖。
|
||||
2. 把现有 schema 收敛到 `schemas/**/*.schema.json`,或在项目文件中显式设置 `GFrameworkConfigSchemaDirectory`。
|
||||
3. 用 `RegisterAllGeneratedConfigTables(...)` 替换启动阶段里手写的逐表注册代码,再按需通过 `GeneratedConfigRegistrationOptions` 补充单表比较器或个别覆盖。
|
||||
4. 继续保留原有运行时消费入口,例如 `GameConfigBootstrap`、`YamlConfigLoader`、`GameConfigHost` 或 `GameConfigRuntime`,避免把“生成注册入口切换”与“运行时读取方式重写”混成同一步。
|
||||
|
||||
当前兼容边界主要有三类:
|
||||
|
||||
- 生成器只覆盖当前公开的 schema 子集;如果现有 schema 使用了尚未支持的结构或元数据,编译期会通过 `ConfigSchemaDiagnostics` 直接暴露,而不会静默退回到手写模型。
|
||||
- `x-gframework-index`、路径元数据和标识符映射都要求满足安全约束;旧项目里依赖宽松命名或不安全路径的配置,需要先整理 schema,再切换到聚合注册入口。
|
||||
- 迁移到生成链路后,运行时读取模型保持不变,但“表类型定义”和“注册目录”会改为编译期输出,因此自定义扩展更适合挂在单表绑定或 `ConfigureLoader` 阶段,而不是继续复制生成器会维护的样板代码。
|
||||
|
||||
如果你需要分批迁移,当前更稳妥地回退是只回退注册入口,而不是同时回退 schema 结构:
|
||||
|
||||
- 可以先让一部分表继续沿手写注册保留,另一部分切到生成的单表绑定或聚合注册。
|
||||
- 如果某张表在迁移后触发诊断,优先根据诊断 ID 收敛 schema,再决定是否临时回到手写注册。
|
||||
- 当项目已经接受 schema 目录约定后,尽量不要再把生成链路和旧的命名 / 路径规则长期并存,否则后续维护会同时承担两套约束。
|
||||
|
||||
## 常见诊断边界
|
||||
|
||||
当前 `ConfigSchemaDiagnostics` 暴露的错误主要分成四类:
|
||||
|
||||
- schema 根对象或 JSON 读入失败
|
||||
- 例如 `GF_ConfigSchema_001`、`GF_ConfigSchema_002`
|
||||
- 主键与类型映射不合法
|
||||
- 例如 `GF_ConfigSchema_003`、`GF_ConfigSchema_005`
|
||||
- 生成标识符或路径元数据不安全
|
||||
- 例如 `GF_ConfigSchema_006`、`GF_ConfigSchema_007`
|
||||
- 额外约束元数据不合法
|
||||
- 例如 `GF_ConfigSchema_008` 到 `GF_ConfigSchema_014`
|
||||
|
||||
这些边界由
|
||||
[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 本身是否超出当前公开子集。
|
||||
|
||||
## 什么时候优先看这页
|
||||
|
||||
适合:
|
||||
|
||||
- 你想确认一个 schema 字段会生成哪些 C# 入口
|
||||
- 你要排查 `x-gframework-index`、路径元数据或标识符冲突
|
||||
- 你在做项目级聚合注册,希望知道 `GeneratedConfigCatalog` 和 `RegisterAllGeneratedConfigTables(...)` 的边界
|
||||
|
||||
不适合:
|
||||
|
||||
- 你只是第一次接入配置运行时
|
||||
- 你更关心 `GameConfigBootstrap`、热重载、Godot 资源路径或 VS Code 配置工具
|
||||
|
||||
这些采用问题分别回到:
|
||||
|
||||
- [游戏内容配置系统](../game/config-system.md)
|
||||
- [VS Code 配置工具](../game/config-tool.md)
|
||||
- [源码生成器总览](./index.md)
|
||||
Loading…
x
Reference in New Issue
Block a user