fix(cqrs): 收口PR审查遗留问题

- 修复并发 CQRS 解析测试的失败路径释放逻辑,并收敛重复 orchestration 以消除新增 analyzer warning

- 更新 generated request invoker provider 相关测试、XML 文档与 generator 注释,明确默认 runtime 的描述符预热契约

- 调整 legacy runtime alias 注册与 generated provider 注册顺序,并同步 cqrs-rewrite 跟踪文档中的 PR #305 triage 结果
This commit is contained in:
gewuyou 2026-04-30 12:58:05 +08:00
parent 0c65cd8e38
commit 0f1e91a499
13 changed files with 170 additions and 104 deletions

View File

@ -306,8 +306,6 @@ public class ArchitectureContextTests
public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently()
{ {
const int workerCount = 8; const int workerCount = 8;
var workerStartupTimeout = TimeSpan.FromSeconds(5);
var firstResolutionTimeout = TimeSpan.FromSeconds(5);
using var startGate = new ManualResetEventSlim(false); using var startGate = new ManualResetEventSlim(false);
using var allowResolutionToComplete = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false);
using var workersReady = new CountdownEvent(workerCount); using var workersReady = new CountdownEvent(workerCount);
@ -339,18 +337,11 @@ public class ArchitectureContextTests
})) }))
.ToArray(); .ToArray();
Assert.That( ReleaseWorkersAfterFirstResolutionAttempt(
workersReady.Wait(workerStartupTimeout), workersReady,
Is.True, startGate,
"Expected all workers to be ready before releasing start gate."); allowResolutionToComplete,
startGate.Set(); () => Volatile.Read(ref resolutionCallCount) > 0);
Assert.That(
SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout),
Is.True,
"Expected at least one CQRS runtime resolution attempt.");
allowResolutionToComplete.Set();
var responses = await Task.WhenAll(requests); var responses = await Task.WhenAll(requests);
@ -372,8 +363,6 @@ public class ArchitectureContextTests
public async Task PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() public async Task PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently()
{ {
const int workerCount = 8; const int workerCount = 8;
var workerStartupTimeout = TimeSpan.FromSeconds(5);
var firstResolutionTimeout = TimeSpan.FromSeconds(5);
using var startGate = new ManualResetEventSlim(false); using var startGate = new ManualResetEventSlim(false);
using var allowResolutionToComplete = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false);
using var workersReady = new CountdownEvent(workerCount); using var workersReady = new CountdownEvent(workerCount);
@ -405,18 +394,11 @@ public class ArchitectureContextTests
})) }))
.ToArray(); .ToArray();
Assert.That( ReleaseWorkersAfterFirstResolutionAttempt(
workersReady.Wait(workerStartupTimeout), workersReady,
Is.True, startGate,
"Expected all workers to be ready before releasing start gate."); allowResolutionToComplete,
startGate.Set(); () => Volatile.Read(ref resolutionCallCount) > 0);
Assert.That(
SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout),
Is.True,
"Expected at least one CQRS runtime resolution attempt.");
allowResolutionToComplete.Set();
await Task.WhenAll(notifications).ConfigureAwait(false); await Task.WhenAll(notifications).ConfigureAwait(false);
@ -437,8 +419,6 @@ public class ArchitectureContextTests
public async Task CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() public async Task CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently()
{ {
const int workerCount = 8; const int workerCount = 8;
var workerStartupTimeout = TimeSpan.FromSeconds(5);
var firstResolutionTimeout = TimeSpan.FromSeconds(5);
using var startGate = new ManualResetEventSlim(false); using var startGate = new ManualResetEventSlim(false);
using var allowResolutionToComplete = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false);
using var workersReady = new CountdownEvent(workerCount); using var workersReady = new CountdownEvent(workerCount);
@ -470,18 +450,11 @@ public class ArchitectureContextTests
})) }))
.ToArray(); .ToArray();
Assert.That( ReleaseWorkersAfterFirstResolutionAttempt(
workersReady.Wait(workerStartupTimeout), workersReady,
Is.True, startGate,
"Expected all workers to be ready before releasing start gate."); allowResolutionToComplete,
startGate.Set(); () => Volatile.Read(ref resolutionCallCount) > 0);
Assert.That(
SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout),
Is.True,
"Expected at least one CQRS runtime resolution attempt.");
allowResolutionToComplete.Set();
await Task.WhenAll(streamTasks).ConfigureAwait(false); await Task.WhenAll(streamTasks).ConfigureAwait(false);
@ -509,6 +482,46 @@ public class ArchitectureContextTests
} }
} }
/// <summary>
/// 释放并发 worker并确保在断言失败时也能放行首次 runtime 解析。
/// </summary>
/// <param name="workersReady">用于确认 worker 已就绪的倒计时器。</param>
/// <param name="startGate">用于同时放行 worker 的门闩。</param>
/// <param name="allowResolutionToComplete">用于解除首次 runtime 解析阻塞的门闩。</param>
/// <param name="hasObservedResolutionAttempt">用于判断当前是否已观察到首次 runtime 解析尝试。</param>
private static void ReleaseWorkersAfterFirstResolutionAttempt(
CountdownEvent workersReady,
ManualResetEventSlim startGate,
ManualResetEventSlim allowResolutionToComplete,
Func<bool> hasObservedResolutionAttempt)
{
ArgumentNullException.ThrowIfNull(workersReady);
ArgumentNullException.ThrowIfNull(startGate);
ArgumentNullException.ThrowIfNull(allowResolutionToComplete);
ArgumentNullException.ThrowIfNull(hasObservedResolutionAttempt);
var workerStartupTimeout = TimeSpan.FromSeconds(5);
var firstResolutionTimeout = TimeSpan.FromSeconds(5);
Assert.That(
workersReady.Wait(workerStartupTimeout),
Is.True,
"Expected all workers to be ready before releasing start gate.");
startGate.Set();
try
{
Assert.That(
SpinWait.SpinUntil(hasObservedResolutionAttempt, firstResolutionTimeout),
Is.True,
"Expected at least one CQRS runtime resolution attempt.");
}
finally
{
allowResolutionToComplete.Set();
}
}
/// <summary> /// <summary>
/// 为 `CreateStream` 并发解析测试提供最小异步流。 /// 为 `CreateStream` 并发解析测试提供最小异步流。
/// </summary> /// </summary>

View File

@ -67,6 +67,23 @@ public sealed partial class CqrsHandlerRegistryGenerator
requestInvokerEmissions); requestInvokerEmissions);
} }
/// <summary>
/// 从 direct handler 注册描述中提取 request invoker 发射计划。
/// </summary>
/// <param name="supportsRequestInvokerProvider">
/// 指示当前 runtime 是否同时暴露 <c>ICqrsRequestInvokerProvider</c> 与
/// <c>IEnumeratesCqrsRequestInvokerDescriptors</c> 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。
/// </param>
/// <param name="registrations">已按稳定顺序整理完成的 handler 注册描述。</param>
/// <returns>
/// 由 <c>directRegistration.RequestInvokerRegistration</c> 派生出的 <see cref="RequestInvokerEmissionSpec" /> 集合。
/// <c>methodIndex</c> 按 <paramref name="registrations" /> 与其 direct registration 的遍历顺序单调递增,
/// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。
/// </returns>
/// <remarks>
/// 缺少 <c>RequestInvokerRegistration</c> 的 direct registration 会被显式跳过,而不会生成半成品 provider 成员;
/// 调用方应把“为什么没有 request invoker registration”对应的诊断留在更早的建模阶段而不是在源码发射阶段兜底。
/// </remarks>
private static ImmutableArray<RequestInvokerEmissionSpec> CreateRequestInvokerEmissions( private static ImmutableArray<RequestInvokerEmissionSpec> CreateRequestInvokerEmissions(
bool supportsRequestInvokerProvider, bool supportsRequestInvokerProvider,
IReadOnlyList<ImplementationRegistrationSpec> registrations) IReadOnlyList<ImplementationRegistrationSpec> registrations)
@ -273,6 +290,17 @@ public sealed partial class CqrsHandlerRegistryGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
} }
/// <summary>
/// 发射 generated registry 的 request invoker provider 成员。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="requestInvokerEmissions">
/// 来自 <see cref="CreateRequestInvokerEmissions(bool, IReadOnlyList{ImplementationRegistrationSpec})" /> 的稳定发射计划。
/// </param>
/// <remarks>
/// 该输出包含三部分描述符数组、provider 查询方法,以及与描述符逐项对应的静态 invoker 方法。
/// 若发射计划为空,调用方应直接跳过整个 provider 分支,而不是输出空的 registry seam。
/// </remarks>
private static void AppendRequestInvokerProviderMembers( private static void AppendRequestInvokerProviderMembers(
StringBuilder builder, StringBuilder builder,
ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions) ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions)
@ -288,6 +316,15 @@ public sealed partial class CqrsHandlerRegistryGenerator
} }
} }
/// <summary>
/// 发射 generated registry 的 request invoker 描述符数组。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="requestInvokerEmissions">当前要输出的 request invoker 发射计划。</param>
/// <remarks>
/// 每个条目都会把请求类型、响应类型和对应的静态 invoker 方法打包成
/// <c>CqrsRequestInvokerDescriptorEntry</c>,供 registrar 在注册阶段写入 dispatcher 的弱缓存。
/// </remarks>
private static void AppendRequestInvokerDescriptorArray( private static void AppendRequestInvokerDescriptorArray(
StringBuilder builder, StringBuilder builder,
ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions) ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions)
@ -319,6 +356,14 @@ public sealed partial class CqrsHandlerRegistryGenerator
builder.AppendLine(" ];"); builder.AppendLine(" ];");
} }
/// <summary>
/// 发射 generated registry 对 request invoker provider 契约的实现方法。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <remarks>
/// 默认 runtime 真正消费的是 <c>GetDescriptors()</c> 暴露的完整描述符集合,并在注册阶段一次性写入缓存;
/// <c>TryGetDescriptor(...)</c> 保留为显式查询接口,因此这里使用线性扫描即可保持生成代码简单且无额外字典分配。
/// </remarks>
private static void AppendRequestInvokerProviderMethods(StringBuilder builder) private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
{ {
builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::"); builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::");
@ -351,6 +396,15 @@ public sealed partial class CqrsHandlerRegistryGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
} }
/// <summary>
/// 为单个 request invoker 描述符发射对应的静态强类型桥接方法。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="emission">当前要输出的 invoker 发射计划。</param>
/// <remarks>
/// 这些方法的编号与 <see cref="RequestInvokerEmissionSpec.MethodIndex" /> 一一对应,
/// dispatcher 通过描述符里的 <see cref="MethodInfo" /> 把 object 形参桥接回强类型 handler 与 request。
/// </remarks>
private static void AppendRequestInvokerMethod(StringBuilder builder, RequestInvokerEmissionSpec emission) private static void AppendRequestInvokerMethod(StringBuilder builder, RequestInvokerEmissionSpec emission)
{ {
builder.Append(" private static global::System.Threading.Tasks.ValueTask<"); builder.Append(" private static global::System.Threading.Tasks.ValueTask<");

View File

@ -62,7 +62,7 @@ internal sealed class CqrsArchitectureContextAdvancedFeaturesTests
} }
[Test] [Test]
public async Task Request_With_Retry_Behavior_Should_Retry_On_Failure() public async Task Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt()
{ {
// 由于我们没有实现实际的重试行为,简化测试逻辑 // 由于我们没有实现实际的重试行为,简化测试逻辑
TestRetryBehavior.AttemptCount = 0; TestRetryBehavior.AttemptCount = 0;
@ -132,7 +132,7 @@ internal sealed class CqrsArchitectureContextAdvancedFeaturesTests
} }
[Test] [Test]
public async Task Transient_Error_Should_Be_Handled_By_Retry_Mechanism() public async Task Transient_Error_Request_Should_Succeed_Without_Simulated_Errors()
{ {
// 由于我们没有实现实际的瞬态错误处理,简化测试逻辑 // 由于我们没有实现实际的瞬态错误处理,简化测试逻辑
TestTransientErrorHandler.ErrorCount = 0; TestTransientErrorHandler.ErrorCount = 0;

View File

@ -67,8 +67,7 @@ public class CqrsArchitectureContextIntegrationTests
[Test] [Test]
public async Task Handler_Can_Access_Architecture_Context() public async Task Handler_Can_Access_Architecture_Context()
{ {
// 当前测试通过直接注入上下文来聚焦验证架构上下文集成结果。 TestContextAwareHandler.LastContext = null;
TestContextAwareHandler.LastContext = _context;
var request = new TestContextAwareRequest(); var request = new TestContextAwareRequest();
await _context!.SendRequestAsync(request).ConfigureAwait(false); await _context!.SendRequestAsync(request).ConfigureAwait(false);
@ -359,17 +358,17 @@ public class CqrsArchitectureContextIntegrationTests
/// <summary> /// <summary>
/// 为上下文感知请求提供静态响应的测试处理器。 /// 为上下文感知请求提供静态响应的测试处理器。
/// </summary> /// </summary>
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string> public sealed class TestContextAwareRequestHandler : ContextAwareBase, IRequestHandler<TestContextAwareRequest, string>
{ {
/// <summary> /// <summary>
/// 处理请求并返回固定结果。 /// 记录当前处理器观察到的架构上下文,并返回固定结果。
/// </summary> /// </summary>
/// <param name="request">当前测试请求。</param> /// <param name="request">当前测试请求。</param>
/// <param name="cancellationToken">取消令牌。</param> /// <param name="cancellationToken">取消令牌。</param>
/// <returns>固定的测试结果。</returns> /// <returns>固定的测试结果。</returns>
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken) public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
{ {
// 保持测试中设置的上下文,不要重置为空。 TestContextAwareHandler.LastContext = Context;
return new ValueTask<string>("Context accessed"); return new ValueTask<string>("Context accessed");
} }
} }

View File

@ -14,12 +14,15 @@ namespace GFramework.Cqrs.Tests.Cqrs;
[NonParallelizable] [NonParallelizable]
internal sealed class CqrsGeneratedRequestInvokerProviderTests internal sealed class CqrsGeneratedRequestInvokerProviderTests
{ {
private ILoggerFactoryProvider? _previousLoggerFactoryProvider;
/// <summary> /// <summary>
/// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。 /// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。
/// </summary> /// </summary>
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
_previousLoggerFactoryProvider = LoggerFactoryResolver.Provider;
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
ClearRegistrarCaches(); ClearRegistrarCaches();
ClearDispatcherCaches(); ClearDispatcherCaches();
@ -31,6 +34,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider ?? new ConsoleLoggerFactoryProvider();
ClearRegistrarCaches(); ClearRegistrarCaches();
ClearDispatcherCaches(); ClearDispatcherCaches();
} }
@ -67,18 +71,7 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
var context = new ArchitectureContext(container); var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")); var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload"));
Assert.That(response, Is.EqualTo("generated:payload"));
var requestBindings = GetDispatcherCacheField("RequestDispatchBindings");
var binding = GetRequestDispatchBindingValue(
requestBindings,
typeof(GeneratedRequestInvokerRequest),
typeof(string));
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo("generated:payload"));
Assert.That(binding, Is.Not.Null);
});
} }
/// <summary> /// <summary>
@ -156,22 +149,4 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
.Invoke(cache, Array.Empty<object>()); .Invoke(cache, Array.Empty<object>());
} }
/// <summary>
/// 读取指定请求/响应类型对当前缓存的 request dispatch binding。
/// </summary>
private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType)
{
var bindingBox = requestBindings.GetType()
.GetMethod("GetValueOrDefaultForTesting", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.Invoke(requestBindings, [requestType, responseType]);
if (bindingBox is null)
{
return null;
}
return bindingBox.GetType()
.GetMethod("Get", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.MakeGenericMethod(responseType)
.Invoke(bindingBox, Array.Empty<object>());
}
} }

View File

@ -5,4 +5,5 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary> /// <summary>
/// 用于验证 generated request invoker provider 接线的测试请求。 /// 用于验证 generated request invoker provider 接线的测试请求。
/// </summary> /// </summary>
/// <param name="Value">用于验证 generated invoker 结果拼接的请求负载。</param>
internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest<string>; internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest<string>;

View File

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

View File

@ -263,8 +263,8 @@ internal static class CqrsHandlerRegistrar
if (registry is not ICqrsRequestInvokerProvider provider) if (registry is not ICqrsRequestInvokerProvider provider)
return; return;
services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider);
RegisterGeneratedRequestInvokerDescriptors(provider, assemblyName, logger); RegisterGeneratedRequestInvokerDescriptors(provider, assemblyName, logger);
services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider);
logger.Debug( logger.Debug(
$"Registered CQRS request invoker provider {provider.GetType().FullName} for assembly {assemblyName}."); $"Registered CQRS request invoker provider {provider.GetType().FullName} for assembly {assemblyName}.");
} }

View File

@ -16,9 +16,10 @@ public interface INotificationPublisher
/// 执行一次通知发布。 /// 执行一次通知发布。
/// </summary> /// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam> /// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前发布调用的处理器集合与执行入口。</param> /// <param name="context">当前发布调用的处理器集合与执行入口,不能为空。</param>
/// <param name="cancellationToken">取消令牌。</param> /// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示通知发布完成的值任务。</returns> /// <returns>表示通知发布完成的值任务。</returns>
/// <exception cref="ArgumentNullException"><paramref name="context" /> 为 <see langword="null" />。</exception>
ValueTask PublishAsync<TNotification>( ValueTask PublishAsync<TNotification>(
NotificationPublishContext<TNotification> context, NotificationPublishContext<TNotification> context,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)

View File

@ -2251,8 +2251,6 @@ public class CqrsHandlerRegistryGeneratorTests
var generatorErrors = execution.GeneratorDiagnostics var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
var generatedSource = execution.GeneratedSources[0].content;
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
@ -2260,6 +2258,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.That(generatorErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That( Assert.That(
generatedSource, generatedSource,
Does.Contain( Does.Contain(
@ -2302,8 +2301,6 @@ public class CqrsHandlerRegistryGeneratorTests
var generatorErrors = execution.GeneratorDiagnostics var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
var generatedSource = execution.GeneratedSources[0].content;
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
@ -2311,6 +2308,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.That(generatorErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That( Assert.That(
generatedSource, generatedSource,
Does.Contain( Does.Contain(
@ -2358,8 +2356,6 @@ public class CqrsHandlerRegistryGeneratorTests
var generatorErrors = execution.GeneratorDiagnostics var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
var generatedSource = execution.GeneratedSources[0].content;
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(inputCompilationErrors, Is.Empty); Assert.That(inputCompilationErrors, Is.Empty);
@ -2367,6 +2363,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.That(generatorErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That( Assert.That(
generatedSource, generatedSource,
Does.Contain( Does.Contain(

View File

@ -98,7 +98,13 @@ public static class CqrsTestRuntime
/// </remarks> /// </remarks>
private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime) private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime)
{ {
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime); if (runtime is not LegacyICqrsRuntime legacyRuntime)
{
throw new InvalidOperationException(
$"The registered {nameof(ICqrsRuntime)} must also implement {typeof(LegacyICqrsRuntime).FullName}.");
}
container.Register<LegacyICqrsRuntime>(legacyRuntime);
} }
/// <summary> /// <summary>

View File

@ -123,10 +123,11 @@ CQRS 迁移与收敛。
- 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断
- 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档
- `2026-04-30` 已重新执行 `$gframework-pr-review` - `2026-04-30` 已重新执行 `$gframework-pr-review`
- 当前分支对应 `PR #304`,状态为 `OPEN` - 当前分支对应 `PR #305`,状态为 `OPEN`
- latest reviewed commit 当前剩余 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads集中在测试脆弱断言、共享测试状态并发保护以及 `CqrsDispatcher` 的缓存线程模型文档 - 当前抓取到 `9` 条 CodeRabbit open threads、`2` 条 Greptile open threads远端 CTRF 汇总为 `2214/2214` passedMegaLinter 仍只暴露 `dotnet-format``Restore operation failed` 环境噪音
- 本地核对后已确认这些评论仍对应当前代码MegaLinter 继续只暴露 `dotnet-format``Restore operation failed` 环境噪音CTRF 汇总为 `2203/2203` passed - 本地核对后,已确认以下评论仍然成立并已完成修正:`ArchitectureContextTests` 并发测试失败路径释放、`CqrsGeneratedRequestInvokerProviderTests` 的全局 logger provider 恢复与私有缓存断言解耦、`CqrsArchitectureContextIntegrationTests` 的真实上下文注入断言、`GeneratedRequestInvokerRequest` / `INotificationPublisher` XML 文档、`CqrsHandlerRegistrar` 的 provider 注册顺序、`CqrsTestRuntime` 的 legacy alias 显式失败模式,以及 `cqrs-rewrite` trace 重复标题
- 已在本地完成 follow-uprequest pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释 - 对于 `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 的数组回归补强: - `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强:
- `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归
- 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()`
@ -212,10 +213,22 @@ CQRS 迁移与收敛。
- `RP-046``RP-062` 的历史验证命令与阶段性结果已移入验证归档active tracking 只保留当前恢复入口需要的最新验证 - `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` - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过 - 结果:通过
- 备注:确认当前分支对应 `PR #304`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread - 备注:确认当前分支对应 `PR #305`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过 - 结果:通过
- 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable``_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题 - 备注:`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` - `bash scripts/validate-csharp-naming.sh`
- 结果:通过 - 结果:通过
- 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归 - 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归

View File

@ -21,7 +21,7 @@
- `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果 - `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果
- `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法 - `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法
### 验证 ### 验证RP-067
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
- 结果:通过,`22/22` passed - 结果:通过,`22/22` passed
@ -30,7 +30,7 @@
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`
### 当前下一步 ### 当前下一步RP-067
1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义 1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义
2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files` 2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files`
@ -53,14 +53,14 @@
- `docs/zh-CN/core/cqrs.md` - `docs/zh-CN/core/cqrs.md`
- 三处文档都已明确:`GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 只是旧命名空间下保留的 compatibility alias新代码应依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` - 三处文档都已明确:`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"` - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过,`42/42` passed - 结果:通过,`42/42` passed
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`
### 当前下一步 ### 当前下一步RP-066
1. 在保持 branch diff 低于阈值的前提下,回到 `dispatch/invoker` 生成前移主线 1. 在保持 branch diff 低于阈值的前提下,回到 `dispatch/invoker` 生成前移主线
2. 优先尝试只覆盖 request 路径的 generated invoker/provider 最小切片,避免一次卷入 notification / stream / pipeline executor 2. 优先尝试只覆盖 request 路径的 generated invoker/provider 最小切片,避免一次卷入 notification / stream / pipeline executor
@ -81,14 +81,14 @@
- `CreateStream(...)` 在并发首次访问时只解析一次 `ICqrsRuntime` - `CreateStream(...)` 在并发首次访问时只解析一次 `ICqrsRuntime`
- 集成后已确认三份测试文件中不再残留 `GFramework.Cqrs.Tests.Mediator` 命名空间或 `Mediator` 语义命名 - 集成后已确认三份测试文件中不再残留 `GFramework.Cqrs.Tests.Mediator` 命名空间或 `Mediator` 语义命名
### 验证 ### 验证RP-065
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"` - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"`
- 结果:通过,`22/22` passed - 结果:通过,`22/22` passed
### 当前下一步 ### 当前下一步RP-065
1. 继续 `Phase 8` 主线,回到 `dispatch/invoker` 生成前移或 `LegacyICqrsRuntime` 收口的下一个低风险切片 1. 继续 `Phase 8` 主线,回到 `dispatch/invoker` 生成前移或 `LegacyICqrsRuntime` 收口的下一个低风险切片
2. 在下一次 batch 结束后复算 branch diff确认距 `50 files` stop condition 的剩余 headroom 2. 在下一次 batch 结束后复算 branch diff确认距 `50 files` stop condition 的剩余 headroom
@ -110,7 +110,7 @@
- `GFramework.Cqrs/README.md``docs/zh-CN/core/cqrs.md` 已同步说明默认通知语义与可替换 seam - `GFramework.Cqrs/README.md``docs/zh-CN/core/cqrs.md` 已同步说明默认通知语义与可替换 seam
- 中途验证曾因并行 .NET 构建产生输出文件锁噪音;已改为串行重跑并获取干净结果 - 中途验证曾因并行 .NET 构建产生输出文件锁噪音;已改为串行重跑并获取干净结果
### 验证 ### 验证RP-064
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`
@ -123,7 +123,7 @@
- `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh` - `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh`
- 结果:通过 - 结果:通过
### 当前下一步 ### 当前下一步RP-064
1. 评估 notification publisher seam 的第二阶段是否需要公开配置面、并行 publisher 或 telemetry decorator 1. 评估 notification publisher seam 的第二阶段是否需要公开配置面、并行 publisher 或 telemetry decorator
2. 把 `dispatch/invoker` 生成前移重新拉回 `Phase 8` 主线,作为下一个实现切片 2. 把 `dispatch/invoker` 生成前移重新拉回 `Phase 8` 主线,作为下一个实现切片
@ -140,7 +140,7 @@
- 当前仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 - 当前仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成
- 本轮把默认下一步从“继续盯 PR thread”调整为“围绕 publisher seam 与 dispatch/invoker 生成前移做下一轮设计收敛” - 本轮把默认下一步从“继续盯 PR thread”调整为“围绕 publisher seam 与 dispatch/invoker 生成前移做下一轮设计收敛”
### 验证 ### 验证RP-063
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`