diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs
index 2d2725b2..e260b380 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs
@@ -185,6 +185,318 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(results, Is.EqualTo([300, 301]));
}
+ ///
+ /// 验证当 generated request invoker provider 返回实例方法时,
+ /// dispatcher 会显式拒绝该描述符,而不是在后续绑定阶段静默接受非法合同。
+ ///
+ [Test]
+ public void SendAsync_Should_Throw_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 exception = Assert.ThrowsAsync(async () =>
+ await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false));
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("non-static invoker method"));
+ }
+
+ ///
+ /// 验证当 generated request invoker provider 返回与 dispatcher 委托签名不兼容的方法时,
+ /// dispatcher 会显式抛出契约错误。
+ ///
+ [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(async () =>
+ await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false));
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("incompatible invoker"));
+ }
+
+ ///
+ /// 验证当 generated stream invoker provider 返回实例方法时,
+ /// dispatcher 会在首次建流时显式拒绝该描述符。
+ ///
+ [Test]
+ public void CreateStream_Should_Throw_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 exception = Assert.ThrowsAsync(async () =>
+ await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false));
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("non-static invoker method"));
+ }
+
+ ///
+ /// 验证当 generated stream invoker provider 返回与 dispatcher 委托签名不兼容的方法时,
+ /// dispatcher 会显式抛出契约错误。
+ ///
+ [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(async () =>
+ await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false));
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("incompatible invoker"));
+ }
+
+ ///
+ /// 模拟返回实例 request invoker 方法的 generated registry。
+ ///
+ private sealed class NonStaticRequestInvokerProviderRegistry :
+ ICqrsHandlerRegistry,
+ ICqrsRequestInvokerProvider,
+ IEnumeratesCqrsRequestInvokerDescriptors
+ {
+ private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new(
+ typeof(GeneratedRequestInvokerRequest),
+ typeof(string),
+ new CqrsRequestInvokerDescriptor(
+ typeof(IRequestHandler),
+ typeof(NonStaticRequestInvokerProviderRegistry).GetMethod(
+ nameof(InvokeGenerated),
+ BindingFlags.NonPublic | BindingFlags.Instance)!));
+
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(IRequestHandler),
+ typeof(GeneratedRequestInvokerRequestHandler));
+ }
+
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out CqrsRequestInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
+ {
+ descriptor = DescriptorEntry.Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return [DescriptorEntry];
+ }
+
+ private ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
+ {
+ return ValueTask.FromResult(string.Empty);
+ }
+ }
+
+ ///
+ /// 模拟返回不兼容 request invoker 方法的 generated registry。
+ ///
+ private sealed class IncompatibleRequestInvokerProviderRegistry :
+ ICqrsHandlerRegistry,
+ ICqrsRequestInvokerProvider,
+ IEnumeratesCqrsRequestInvokerDescriptors
+ {
+ private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new(
+ typeof(GeneratedRequestInvokerRequest),
+ typeof(string),
+ new CqrsRequestInvokerDescriptor(
+ typeof(IRequestHandler),
+ typeof(IncompatibleRequestInvokerProviderRegistry).GetMethod(
+ nameof(InvokeGenerated),
+ BindingFlags.NonPublic | BindingFlags.Static)!));
+
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(IRequestHandler),
+ typeof(GeneratedRequestInvokerRequestHandler));
+ }
+
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out CqrsRequestInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
+ {
+ descriptor = DescriptorEntry.Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return [DescriptorEntry];
+ }
+
+ private static string InvokeGenerated(object handler, object request)
+ {
+ return string.Empty;
+ }
+ }
+
+ ///
+ /// 模拟返回实例 stream invoker 方法的 generated registry。
+ ///
+ private sealed class NonStaticStreamInvokerProviderRegistry :
+ ICqrsHandlerRegistry,
+ ICqrsStreamInvokerProvider,
+ IEnumeratesCqrsStreamInvokerDescriptors
+ {
+ private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
+ typeof(GeneratedStreamInvokerRequest),
+ typeof(int),
+ new CqrsStreamInvokerDescriptor(
+ typeof(IStreamRequestHandler),
+ typeof(NonStaticStreamInvokerProviderRegistry).GetMethod(
+ nameof(InvokeGenerated),
+ BindingFlags.NonPublic | BindingFlags.Instance)!));
+
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(IStreamRequestHandler),
+ typeof(GeneratedStreamInvokerRequestHandler));
+ }
+
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out CqrsStreamInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
+ {
+ descriptor = DescriptorEntry.Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return [DescriptorEntry];
+ }
+
+ private object InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
+ {
+ return Array.Empty().ToAsyncEnumerable();
+ }
+ }
+
+ ///
+ /// 模拟返回不兼容 stream invoker 方法的 generated registry。
+ ///
+ private sealed class IncompatibleStreamInvokerProviderRegistry :
+ ICqrsHandlerRegistry,
+ ICqrsStreamInvokerProvider,
+ IEnumeratesCqrsStreamInvokerDescriptors
+ {
+ private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
+ typeof(GeneratedStreamInvokerRequest),
+ typeof(int),
+ new CqrsStreamInvokerDescriptor(
+ typeof(IStreamRequestHandler),
+ typeof(IncompatibleStreamInvokerProviderRegistry).GetMethod(
+ nameof(InvokeGenerated),
+ BindingFlags.NonPublic | BindingFlags.Static)!));
+
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddTransient(
+ typeof(IStreamRequestHandler),
+ typeof(GeneratedStreamInvokerRequestHandler));
+ }
+
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out CqrsStreamInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
+ {
+ descriptor = DescriptorEntry.Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return [DescriptorEntry];
+ }
+
+ private static object InvokeGenerated(object handler, object request)
+ {
+ return Array.Empty().ToAsyncEnumerable();
+ }
+ }
+
///
/// 创建带有 generated request invoker registry 元数据的程序集替身。
///
@@ -215,6 +527,27 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
return generatedAssembly;
}
+ ///
+ /// 创建带有指定 generated registry 元数据的程序集替身。
+ ///
+ /// 测试 registry 类型。
+ /// 模拟程序集全名。
+ /// 可用于 registrar 注册流程的程序集替身。
+ private static Mock CreateGeneratedAssembly(Type registryType, string assemblyFullName)
+ {
+ ArgumentNullException.ThrowIfNull(registryType);
+ ArgumentException.ThrowIfNullOrWhiteSpace(assemblyFullName);
+
+ var generatedAssembly = new Mock();
+ generatedAssembly
+ .SetupGet(static assembly => assembly.FullName)
+ .Returns(assemblyFullName);
+ generatedAssembly
+ .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
+ .Returns([new CqrsHandlerRegistryAttribute(registryType)]);
+ return generatedAssembly;
+ }
+
///
/// 创建带有 hidden implementation request invoker registry 元数据的程序集替身。
///
diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs
index 17cbb67a..0fa91f76 100644
--- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs
+++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs
@@ -256,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), descriptor.InvokerMethod) is not
- RequestInvoker invoker)
+ try
+ {
+ if (Delegate.CreateDelegate(typeof(RequestInvoker), descriptor.InvokerMethod) is not
+ RequestInvoker 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(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(descriptor.HandlerType, invoker);
}
///
@@ -328,13 +337,22 @@ internal sealed class CqrsDispatcher(
$"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}.");
}
- if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker)
+ 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}.");
+ $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.",
+ exception);
}
-
- return new StreamInvokerDescriptor(descriptor.HandlerType, invoker);
}
///
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index e0c247ec..271ff6fc 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-072`
+- 恢复点编号:`CQRS-REWRITE-RP-073`
- 当前阶段:`Phase 8`
- 当前焦点:
- 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
@@ -74,6 +74,10 @@ CQRS 迁移与收敛。
- 已完成一轮 invoker provider gate 合同回归:
- `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射
- 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐
+ - 已完成一轮 generated invoker provider runtime 失败边界修复:
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,锁定 dispatcher 在首次绑定阶段会显式拒绝非法 generated descriptor
+ - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现把 `Delegate.CreateDelegate(...)` 抛出的 `ArgumentException` 统一包装为已有 XML 文档承诺的 `InvalidOperationException`,保持 request / stream 两条错误消息语义一致
+ - 本轮顺手为新增异步断言补齐 `ConfigureAwait(false)`,消除新测试引入的 `MA0004` warning
- 当前相对 `origin/main` 的累计 branch diff 为 `24 files / 1754 changed lines`,仍低于本轮 `$gframework-batch-boot 50` 的主要 stop condition,可继续推进下一批低风险切片
- 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
@@ -297,6 +301,12 @@ CQRS 迁移与收敛。
- `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.Core/GFramework.Core.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题
@@ -330,6 +340,6 @@ CQRS 迁移与收敛。
## 下一步
-1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的 runtime 失败边界或 generator gate 合同补强
+1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余 runtime 失败边界或 generator gate 合同补强
2. 基于已落地的 notification publisher seam,评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator
3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 91f372d4..674ca8a1 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,31 @@
## 2026-04-30
+### 阶段:generated invoker provider runtime 失败边界修复(CQRS-REWRITE-RP-073)
+
+- 在 `RP-072` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,文件阈值 headroom 依然充足,因此继续推进下一批 runtime 失败边界回归
+- 本轮原计划只补 `CqrsGeneratedRequestInvokerProviderTests` 的 request / stream 非 happy-path 回归,但定向测试首轮直接暴露出一个真实 runtime 缺口:
+ - `CqrsDispatcher.CreateRequestInvokerDescriptor(...)` 与 `CreateStreamInvokerDescriptor(...)` 的 XML 文档和消息语义都承诺会抛 `InvalidOperationException`
+ - 实际实现先调用 `Delegate.CreateDelegate(...)`,当 invoker 签名不兼容时会直接冒出 `ArgumentException`,导致文档承诺与运行时行为不一致
+- 主线程已完成:
+ - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,并保留 request / stream happy-path 作为同批守护断言
+ - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现对 request / stream 两条 descriptor 创建路径统一捕获 `ArgumentException`,并转换成带原有错误消息的 `InvalidOperationException`
+ - 新增异步断言已补齐 `ConfigureAwait(false)`,避免测试批次自身引入 `MA0004` analyzer warning
+
+### 验证(RP-073)
+
+- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
+ - 结果:通过
+ - 备注:并行执行 build/test 时曾出现 `MSB3026` 输出文件竞争噪音;无真实编译失败,也未引入新增 analyzer warning
+- `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
+
+### 当前下一步(RP-073)
+
+1. 先提交本轮 runtime 失败边界修复与恢复点更新
+2. 重新复算 branch diff 后,再判断是否继续推进剩余 provider 失败边界或在接近阈值前停下
+3. 若继续下一批,优先保持单文件或双文件写集,避免在本轮后段扩散 review 面积
+
### 阶段:invoker provider gate 合同回归(CQRS-REWRITE-RP-072)
- 在 `RP-071` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,未接近主要 stop condition,因此继续追加一轮 test-only generator 合同回归