mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(cqrs): 收敛 generated invoker 异常语义
- 修复 request 与 stream generated invoker 签名不兼容时冒出 ArgumentException 的行为,统一包装为 InvalidOperationException - 补充对应 runtime 回归测试并更新 CQRS 重写恢复点到 RP-073
This commit is contained in:
parent
502f65239c
commit
1091594224
@ -185,6 +185,318 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
Assert.That(results, Is.EqualTo([300, 301]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 generated request invoker provider 返回实例方法时,
|
||||
/// dispatcher 会显式拒绝该描述符,而不是在后续绑定阶段静默接受非法合同。
|
||||
/// </summary>
|
||||
[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<InvalidOperationException>(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"));
|
||||
}
|
||||
|
||||
/// <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 返回实例方法时,
|
||||
/// dispatcher 会在首次建流时显式拒绝该描述符。
|
||||
/// </summary>
|
||||
[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<InvalidOperationException>(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"));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 模拟返回实例 request invoker 方法的 generated registry。
|
||||
/// </summary>
|
||||
private sealed class NonStaticRequestInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsRequestInvokerProvider,
|
||||
IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new(
|
||||
typeof(GeneratedRequestInvokerRequest),
|
||||
typeof(string),
|
||||
new CqrsRequestInvokerDescriptor(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(NonStaticRequestInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Instance)!));
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||
{
|
||||
descriptor = DescriptorEntry.Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return [DescriptorEntry];
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new(
|
||||
typeof(GeneratedStreamInvokerRequest),
|
||||
typeof(int),
|
||||
new CqrsStreamInvokerDescriptor(
|
||||
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||
typeof(NonStaticStreamInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Instance)!));
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||
{
|
||||
descriptor = DescriptorEntry.Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return [DescriptorEntry];
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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>
|
||||
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
||||
/// </summary>
|
||||
@ -215,6 +527,27 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
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>
|
||||
|
||||
@ -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<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>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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 与专门测试,可暂时移出最高优先级
|
||||
|
||||
@ -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 合同回归
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user