mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 20:38:58 +08:00
fix(core): 收口 legacy bridge 同步评审问题
- 修复 legacy 同步 bridge 的 runtime 等待方式,统一通过共享 helper 隔离同步上下文并收口重复 dispatch-context 解析逻辑 - 补充 legacy async command bridge 的取消可见性,并更新 ICqrsRuntime 与相关入口的契约说明 - 新增 bridge 回归测试并更新 cqrs-rewrite active tracking,覆盖同步上下文隔离、测试容器释放与取消语义
This commit is contained in:
parent
6056159866
commit
dc3bd3744e
@ -43,6 +43,7 @@ namespace GFramework.Core.Tests.Architectures;
|
|||||||
/// - GetUtility方法 - 获取未注册工具时抛出异常
|
/// - GetUtility方法 - 获取未注册工具时抛出异常
|
||||||
/// - GetEnvironment方法 - 获取环境对象
|
/// - GetEnvironment方法 - 获取环境对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[NonParallelizable]
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class ArchitectureContextTests
|
public class ArchitectureContextTests
|
||||||
{
|
{
|
||||||
@ -150,13 +151,20 @@ public class ArchitectureContextTests
|
|||||||
{
|
{
|
||||||
LegacyBridgePipelineTracker.Reset();
|
LegacyBridgePipelineTracker.Reset();
|
||||||
var testQuery = new LegacyArchitectureBridgeQuery();
|
var testQuery = new LegacyArchitectureBridgeQuery();
|
||||||
var bridgeContext = CreateFrozenBridgeContext();
|
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||||
|
|
||||||
var result = bridgeContext.SendQuery(testQuery);
|
try
|
||||||
|
{
|
||||||
|
var result = bridgeContext.SendQuery(testQuery);
|
||||||
|
|
||||||
Assert.That(result, Is.EqualTo(24));
|
Assert.That(result, Is.EqualTo(24));
|
||||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bridgeContainer.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -190,13 +198,20 @@ public class ArchitectureContextTests
|
|||||||
{
|
{
|
||||||
LegacyBridgePipelineTracker.Reset();
|
LegacyBridgePipelineTracker.Reset();
|
||||||
var testCommand = new LegacyArchitectureBridgeCommand();
|
var testCommand = new LegacyArchitectureBridgeCommand();
|
||||||
var bridgeContext = CreateFrozenBridgeContext();
|
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||||
|
|
||||||
bridgeContext.SendCommand(testCommand);
|
try
|
||||||
|
{
|
||||||
|
bridgeContext.SendCommand(testCommand);
|
||||||
|
|
||||||
Assert.That(testCommand.Executed, Is.True);
|
Assert.That(testCommand.Executed, Is.True);
|
||||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bridgeContainer.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -230,13 +245,20 @@ public class ArchitectureContextTests
|
|||||||
{
|
{
|
||||||
LegacyBridgePipelineTracker.Reset();
|
LegacyBridgePipelineTracker.Reset();
|
||||||
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
|
var testCommand = new LegacyArchitectureBridgeCommandWithResult();
|
||||||
var bridgeContext = CreateFrozenBridgeContext();
|
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||||
|
|
||||||
var result = bridgeContext.SendCommand(testCommand);
|
try
|
||||||
|
{
|
||||||
|
var result = bridgeContext.SendCommand(testCommand);
|
||||||
|
|
||||||
Assert.That(result, Is.EqualTo(42));
|
Assert.That(result, Is.EqualTo(42));
|
||||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bridgeContainer.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -247,22 +269,30 @@ public class ArchitectureContextTests
|
|||||||
{
|
{
|
||||||
LegacyBridgePipelineTracker.Reset();
|
LegacyBridgePipelineTracker.Reset();
|
||||||
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
|
var testQuery = new LegacyArchitectureBridgeAsyncQuery();
|
||||||
var bridgeContext = CreateFrozenBridgeContext();
|
var bridgeContext = CreateFrozenBridgeContext(out var bridgeContainer);
|
||||||
|
|
||||||
var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
|
try
|
||||||
|
{
|
||||||
|
var result = await bridgeContext.SendQueryAsync(testQuery).ConfigureAwait(false);
|
||||||
|
|
||||||
Assert.That(result, Is.EqualTo(64));
|
Assert.That(result, Is.EqualTo(64));
|
||||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bridgeContainer.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
|
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="container">返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。</param>
|
||||||
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
|
/// <returns>能够执行 legacy bridge request 且会 materialize open-generic pipeline behavior 的上下文。</returns>
|
||||||
private static ArchitectureContext CreateFrozenBridgeContext()
|
private static ArchitectureContext CreateFrozenBridgeContext(out MicrosoftDiContainer container)
|
||||||
{
|
{
|
||||||
var container = new MicrosoftDiContainer();
|
container = new MicrosoftDiContainer();
|
||||||
RegisterLegacyBridgeHandlers(container);
|
RegisterLegacyBridgeHandlers(container);
|
||||||
new CqrsRuntimeModule().Register(container);
|
new CqrsRuntimeModule().Register(container);
|
||||||
container.ExecuteServicesHook(services =>
|
container.ExecuteServicesHook(services =>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ namespace GFramework.Core.Tests.Architectures;
|
|||||||
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
|
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
|
||||||
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
|
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[NonParallelizable]
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class ArchitectureModulesBehaviorTests
|
public class ArchitectureModulesBehaviorTests
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
using GFramework.Core.Command;
|
using GFramework.Core.Command;
|
||||||
|
using GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Command;
|
namespace GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
@ -75,6 +76,59 @@ public class CommandExecutorTests
|
|||||||
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send<int>(null!));
|
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send<int>(null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 legacy 同步命令桥接会在线程池上等待 runtime,
|
||||||
|
/// 避免直接继承调用方当前的同步上下文。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
|
||||||
|
{
|
||||||
|
var runtime = new RecordingCqrsRuntime();
|
||||||
|
var executor = new CommandExecutor(runtime);
|
||||||
|
var command = new ContextAwareLegacyCommand();
|
||||||
|
var expectedContext = new TestArchitectureContextBaseStub();
|
||||||
|
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
|
||||||
|
var originalContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
|
||||||
|
|
||||||
|
executor.Send(command);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandDispatchRequest>());
|
||||||
|
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(originalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context()
|
||||||
|
{
|
||||||
|
var runtime = new RecordingCqrsRuntime(static _ => 123);
|
||||||
|
var executor = new CommandExecutor(runtime);
|
||||||
|
var command = new ContextAwareLegacyCommandWithResult(123);
|
||||||
|
var expectedContext = new TestArchitectureContextBaseStub();
|
||||||
|
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
|
||||||
|
|
||||||
|
var result = executor.Send(command);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result, Is.EqualTo(123));
|
||||||
|
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandResultDispatchRequest>());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试SendAsync方法执行异步命令
|
/// 测试SendAsync方法执行异步命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -122,4 +176,11 @@ public class CommandExecutorTests
|
|||||||
{
|
{
|
||||||
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
|
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为同步 bridge 测试提供最小架构上下文替身。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
Normal file
31
GFramework.Core.Tests/Command/ContextAwareLegacyCommand.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Command;
|
||||||
|
using GFramework.Core.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的 legacy 命令。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ContextAwareLegacyCommand : ContextAwareBase, ICommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取执行期间观察到的架构上下文。
|
||||||
|
/// </summary>
|
||||||
|
public IArchitectureContext? ObservedContext { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取命令是否已经执行。
|
||||||
|
/// </summary>
|
||||||
|
public bool Executed { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
Executed = true;
|
||||||
|
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Command;
|
||||||
|
using GFramework.Core.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="CommandExecutorTests" /> 提供可观察上下文注入的带返回值 legacy 命令。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ContextAwareLegacyCommandWithResult(int result) : ContextAwareBase, ICommand<int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取执行期间观察到的架构上下文。
|
||||||
|
/// </summary>
|
||||||
|
public IArchitectureContext? ObservedContext { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Execute()
|
||||||
|
{
|
||||||
|
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
Normal file
66
GFramework.Core.Tests/Command/RecordingCqrsRuntime.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 bridge 执行线程与收到请求的最小 CQRS runtime 测试替身。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RecordingCqrsRuntime(Func<object?, object?>? responseFactory = null) : ICqrsRuntime
|
||||||
|
{
|
||||||
|
private static readonly Func<object?, object?> DefaultResponseFactory = _ => null;
|
||||||
|
|
||||||
|
private readonly Func<object?, object?> _responseFactory = responseFactory ?? DefaultResponseFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最近一次 <see cref="SendAsync{TResponse}" /> 观察到的同步上下文类型。
|
||||||
|
/// </summary>
|
||||||
|
public Type? ObservedSynchronizationContextType { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最近一次收到的请求实例。
|
||||||
|
/// </summary>
|
||||||
|
public object? LastRequest { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ValueTask<TResponse> SendAsync<TResponse>(
|
||||||
|
ICqrsContext context,
|
||||||
|
IRequest<TResponse> request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
ObservedSynchronizationContextType = SynchronizationContext.Current?.GetType();
|
||||||
|
LastRequest = request;
|
||||||
|
|
||||||
|
object? response = request switch
|
||||||
|
{
|
||||||
|
IRequest<Unit> => Unit.Value,
|
||||||
|
_ => _responseFactory(request)
|
||||||
|
};
|
||||||
|
|
||||||
|
return ValueTask.FromResult((TResponse)response!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ValueTask PublishAsync<TNotification>(
|
||||||
|
ICqrsContext context,
|
||||||
|
TNotification notification,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
where TNotification : INotification
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||||||
|
ICqrsContext context,
|
||||||
|
IStreamRequest<TResponse> request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 legacy 同步 bridge 回归测试提供可识别的同步上下文占位类型。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TestLegacySynchronizationContext : SynchronizationContext
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.Abstractions.Command;
|
||||||
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
using GFramework.Core.Cqrs;
|
||||||
|
using GFramework.Core.Rule;
|
||||||
|
using GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 legacy 异步无返回值命令 bridge handler 的取消语义。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class LegacyAsyncCommandDispatchRequestHandlerTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当取消令牌在执行前已触发时,handler 不会启动底层 legacy 命令。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_Throw_Without_Executing_Command_When_Cancellation_Is_Already_Requested()
|
||||||
|
{
|
||||||
|
var handler = new LegacyAsyncCommandDispatchRequestHandler();
|
||||||
|
var command = new ProbeAsyncCommand(Task.CompletedTask);
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<OperationCanceledException>(
|
||||||
|
async () => await handler.Handle(
|
||||||
|
new LegacyAsyncCommandDispatchRequest(command),
|
||||||
|
cancellationTokenSource.Token)
|
||||||
|
.AsTask()
|
||||||
|
.ConfigureAwait(false));
|
||||||
|
Assert.That(command.ExecutionCount, Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当底层 legacy 命令正在运行时,handler 会通过 <c>WaitAsync</c> 及时向调用方暴露取消。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task Handle_Should_Observe_Cancellation_While_Command_Is_Running()
|
||||||
|
{
|
||||||
|
var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var handler = new LegacyAsyncCommandDispatchRequestHandler();
|
||||||
|
var command = new ProbeAsyncCommand(completionSource.Task);
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
((IContextAware)handler).SetContext(new TestArchitectureContextBaseStub());
|
||||||
|
|
||||||
|
var handleTask = handler.Handle(
|
||||||
|
new LegacyAsyncCommandDispatchRequest(command),
|
||||||
|
cancellationTokenSource.Token)
|
||||||
|
.AsTask();
|
||||||
|
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
async () => await handleTask.ConfigureAwait(false),
|
||||||
|
Throws.InstanceOf<OperationCanceledException>());
|
||||||
|
Assert.That(command.ExecutionCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 handler 取消测试提供可控完成时机的异步命令替身。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ProbeAsyncCommand(Task executionTask) : ContextAwareBase, IAsyncCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取底层命令逻辑的触发次数。
|
||||||
|
/// </summary>
|
||||||
|
public int ExecutionCount { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
ExecutionCount++;
|
||||||
|
return executionTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 handler 取消测试提供最小架构上下文替身。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
using GFramework.Core.Query;
|
using GFramework.Core.Query;
|
||||||
|
using GFramework.Core.Tests.Architectures;
|
||||||
|
using GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Query;
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
@ -138,4 +140,32 @@ public class AsyncQueryExecutorTests
|
|||||||
Assert.That(result1, Is.EqualTo(20));
|
Assert.That(result1, Is.EqualTo(20));
|
||||||
Assert.That(result2, Is.EqualTo(40));
|
Assert.That(result2, Is.EqualTo(40));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 legacy 异步查询桥接会保留上下文注入,并通过 runtime 返回结果。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task SendAsync_Should_Bridge_Through_Runtime_And_Preserve_Context()
|
||||||
|
{
|
||||||
|
var runtime = new RecordingCqrsRuntime(static _ => 64);
|
||||||
|
var executor = new AsyncQueryExecutor(runtime);
|
||||||
|
var query = new ContextAwareLegacyAsyncQuery(64);
|
||||||
|
var expectedContext = new TestArchitectureContextBaseStub();
|
||||||
|
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
|
||||||
|
|
||||||
|
var result = await executor.SendAsync(query);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result, Is.EqualTo(64));
|
||||||
|
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyAsyncQueryDispatchRequest>());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为异步 bridge 测试提供最小架构上下文替身。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
Normal file
26
GFramework.Core.Tests/Query/ContextAwareLegacyAsyncQuery.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Query;
|
||||||
|
using GFramework.Core.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="AsyncQueryExecutorTests" /> 提供可观察上下文注入的 legacy 异步查询。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ContextAwareLegacyAsyncQuery(int result) : ContextAwareBase, IAsyncQuery<int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取执行期间观察到的架构上下文。
|
||||||
|
/// </summary>
|
||||||
|
public IArchitectureContext? ObservedContext { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<int> DoAsync()
|
||||||
|
{
|
||||||
|
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
Normal file
26
GFramework.Core.Tests/Query/ContextAwareLegacyQuery.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Query;
|
||||||
|
using GFramework.Core.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="QueryExecutorTests" /> 提供可观察上下文注入的 legacy 查询。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ContextAwareLegacyQuery(int result) : ContextAwareBase, IQuery<int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取执行期间观察到的架构上下文。
|
||||||
|
/// </summary>
|
||||||
|
public IArchitectureContext? ObservedContext { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Do()
|
||||||
|
{
|
||||||
|
ObservedContext = ((GFramework.Core.Abstractions.Rule.IContextAware)this).GetContext();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
using GFramework.Core.Query;
|
using GFramework.Core.Query;
|
||||||
|
using GFramework.Core.Tests.Architectures;
|
||||||
|
using GFramework.Core.Tests.Command;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Query;
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
@ -61,4 +63,44 @@ public class QueryExecutorTests
|
|||||||
|
|
||||||
Assert.That(result, Is.EqualTo("Result: 10"));
|
Assert.That(result, Is.EqualTo("Result: 10"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 legacy 同步查询桥接会在线程池上等待 runtime,
|
||||||
|
/// 避免直接复用调用方的同步上下文。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
|
||||||
|
{
|
||||||
|
var runtime = new RecordingCqrsRuntime(static _ => 24);
|
||||||
|
var executor = new QueryExecutor(runtime);
|
||||||
|
var query = new ContextAwareLegacyQuery(24);
|
||||||
|
var expectedContext = new TestArchitectureContextBaseStub();
|
||||||
|
((GFramework.Core.Abstractions.Rule.IContextAware)query).SetContext(expectedContext);
|
||||||
|
var originalContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
|
||||||
|
|
||||||
|
var result = executor.Send(query);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result, Is.EqualTo(24));
|
||||||
|
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyQueryDispatchRequest>());
|
||||||
|
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(originalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为同步 bridge 测试提供最小架构上下文替身。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,8 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
/// <returns>响应结果</returns>
|
/// <returns>响应结果</returns>
|
||||||
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
|
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
|
||||||
{
|
{
|
||||||
return SendRequestAsync(request).AsTask().GetAwaiter().GetResult();
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -197,7 +198,8 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
/// <returns>查询结果</returns>
|
/// <returns>查询结果</returns>
|
||||||
public TResponse SendQuery<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
|
public TResponse SendQuery<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
|
||||||
{
|
{
|
||||||
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -403,7 +405,8 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
/// <returns>命令执行结果</returns>
|
/// <returns>命令执行结果</returns>
|
||||||
public TResponse SendCommand<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
|
public TResponse SendCommand<TResponse>(global::GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
|
||||||
{
|
{
|
||||||
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
// Copyright (c) 2025-2026 GeWuYou
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
|
||||||
using GFramework.Core.Cqrs;
|
using GFramework.Core.Cqrs;
|
||||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
|
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
|
||||||
@ -78,9 +76,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
|
||||||
if (TryResolveDispatchContext(command, out var context))
|
var cqrsRuntime = _runtime;
|
||||||
|
|
||||||
|
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
|
||||||
{
|
{
|
||||||
return _runtime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
|
return cqrsRuntime.SendAsync(context, new LegacyAsyncCommandDispatchRequest(command)).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.ExecuteAsync();
|
return command.ExecuteAsync();
|
||||||
@ -97,9 +97,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
|
||||||
if (TryResolveDispatchContext(command, out var context))
|
var cqrsRuntime = _runtime;
|
||||||
|
|
||||||
|
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, command, out var context))
|
||||||
{
|
{
|
||||||
return BridgeAsyncCommandWithResultAsync(_runtime, context, command);
|
return BridgeAsyncCommandWithResultAsync(cqrsRuntime, context, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.ExecuteAsync();
|
return command.ExecuteAsync();
|
||||||
@ -119,12 +121,14 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
|||||||
where TTarget : class
|
where TTarget : class
|
||||||
where TRequest : IRequest<Unit>
|
where TRequest : IRequest<Unit>
|
||||||
{
|
{
|
||||||
if (!TryResolveDispatchContext(target, out var context))
|
var cqrsRuntime = _runtime;
|
||||||
|
|
||||||
|
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
|
LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,13 +149,15 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
|||||||
where TTarget : class
|
where TTarget : class
|
||||||
where TRequest : IRequest<object?>
|
where TRequest : IRequest<object?>
|
||||||
{
|
{
|
||||||
if (!TryResolveDispatchContext(target, out var context))
|
var cqrsRuntime = _runtime;
|
||||||
|
|
||||||
|
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var boxedResult = _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
|
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||||
result = (TResult)boxedResult!;
|
result = (TResult)boxedResult!;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -177,33 +183,4 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return (TResult)boxedResult!;
|
return (TResult)boxedResult!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 解析当前 legacy 目标对象应该绑定到哪个架构上下文。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
|
||||||
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
|
|
||||||
/// <returns>如果既接入了 runtime 且目标对象提供了上下文,则返回 <see langword="true" />。</returns>
|
|
||||||
[MemberNotNullWhen(true, nameof(_runtime))]
|
|
||||||
private bool TryResolveDispatchContext(
|
|
||||||
object target,
|
|
||||||
out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
|
||||||
{
|
|
||||||
context = null!;
|
|
||||||
|
|
||||||
if (_runtime is null || target is not IContextAware contextAware)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context = contextAware.GetContext();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,10 @@ internal sealed class LegacyAsyncCommandDispatchRequestHandler
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
// Legacy ExecuteAsync contract does not accept CancellationToken; use WaitAsync so the caller can still observe cancellation promptly.
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
PrepareTarget(request.Command);
|
PrepareTarget(request.Command);
|
||||||
await request.Command.ExecuteAsync().ConfigureAwait(false);
|
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return Unit.Value;
|
return Unit.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
Normal file
94
GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 legacy Core CQRS bridge 提供共享的上下文解析与同步兼容辅助逻辑。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 旧的同步 Command/Query 入口仍需要阻塞等待统一 <see cref="ICqrsRuntime" /> 返回结果。
|
||||||
|
/// 这里统一通过 <see cref="Task.Run(System.Func{System.Threading.Tasks.Task})" /> 把等待动作切换到线程池,
|
||||||
|
/// 避免直接占用调用方的 <see cref="SynchronizationContext" /> 导致 legacy 同步入口与异步 pipeline 互相卡死。
|
||||||
|
/// </remarks>
|
||||||
|
internal static class LegacyCqrsDispatchHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 解析当前 legacy 目标对象是否能够绑定到统一 CQRS runtime 的架构上下文。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">当前执行器可用的统一 CQRS runtime。</param>
|
||||||
|
/// <param name="target">即将执行的 legacy 目标对象。</param>
|
||||||
|
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
|
||||||
|
/// <returns>
|
||||||
|
/// 当 <paramref name="runtime" /> 可用且 <paramref name="target" /> 能稳定提供
|
||||||
|
/// <see cref="IArchitectureContext" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。
|
||||||
|
/// </returns>
|
||||||
|
internal static bool TryResolveDispatchContext(
|
||||||
|
[NotNullWhen(true)] ICqrsRuntime? runtime,
|
||||||
|
object target,
|
||||||
|
out IArchitectureContext context)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
|
||||||
|
context = null!;
|
||||||
|
|
||||||
|
if (runtime is null || target is not IContextAware contextAware)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context = contextAware.GetContext();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同步等待统一 CQRS runtime 完成无返回值请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
|
||||||
|
/// <param name="context">当前架构上下文。</param>
|
||||||
|
/// <param name="request">要同步等待的请求。</param>
|
||||||
|
internal static void SendSynchronously(
|
||||||
|
ICqrsRuntime runtime,
|
||||||
|
IArchitectureContext context,
|
||||||
|
IRequest<Unit> request)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同步等待统一 CQRS runtime 完成带返回值请求,并返回实际响应。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">请求响应类型。</typeparam>
|
||||||
|
/// <param name="runtime">负责分发当前请求的统一 CQRS runtime。</param>
|
||||||
|
/// <param name="context">当前架构上下文。</param>
|
||||||
|
/// <param name="request">要同步等待的请求。</param>
|
||||||
|
/// <returns>统一 CQRS runtime 返回的响应结果。</returns>
|
||||||
|
internal static TResponse SendSynchronously<TResponse>(
|
||||||
|
ICqrsRuntime runtime,
|
||||||
|
IArchitectureContext context,
|
||||||
|
IRequest<TResponse> request)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
return Task.Run(() => runtime.SendAsync(context, request).AsTask()).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,7 @@
|
|||||||
// Copyright (c) 2025-2026 GeWuYou
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using GFramework.Core.Abstractions.Query;
|
using GFramework.Core.Abstractions.Query;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
|
||||||
using GFramework.Core.Cqrs;
|
using GFramework.Core.Cqrs;
|
||||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
|
|
||||||
@ -31,9 +29,11 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
if (TryResolveDispatchContext(query, out var context))
|
var cqrsRuntime = _runtime;
|
||||||
|
|
||||||
|
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||||
{
|
{
|
||||||
return BridgeAsyncQueryAsync(_runtime, context, query);
|
return BridgeAsyncQueryAsync(cqrsRuntime, context, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.DoAsync();
|
return query.DoAsync();
|
||||||
@ -61,32 +61,4 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
|
|||||||
return (TResult)boxedResult!;
|
return (TResult)boxedResult!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 解析当前 legacy 查询应该绑定到哪个架构上下文。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">即将执行的 legacy 查询对象。</param>
|
|
||||||
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
|
|
||||||
/// <returns>如果既接入了 runtime 且查询对象提供了上下文,则返回 <see langword="true" />。</returns>
|
|
||||||
[MemberNotNullWhen(true, nameof(_runtime))]
|
|
||||||
private bool TryResolveDispatchContext(
|
|
||||||
object query,
|
|
||||||
out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
|
||||||
{
|
|
||||||
context = null!;
|
|
||||||
|
|
||||||
if (_runtime is null || query is not IContextAware contextAware)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context = contextAware.GetContext();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
// Copyright (c) 2025-2026 GeWuYou
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using GFramework.Core.Abstractions.Query;
|
using GFramework.Core.Abstractions.Query;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
|
||||||
using GFramework.Core.Cqrs;
|
using GFramework.Core.Cqrs;
|
||||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
|
|
||||||
@ -31,53 +29,30 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TResult">查询结果的类型。</typeparam>
|
/// <typeparam name="TResult">查询结果的类型。</typeparam>
|
||||||
/// <param name="query">要执行的查询对象,必须实现 IQuery<TResult> 接口。</param>
|
/// <param name="query">要执行的查询对象,必须实现 IQuery<TResult> 接口。</param>
|
||||||
/// <returns>查询执行的结果,类型为 TResult。</returns>
|
/// <returns>查询执行成功后还原出的 <typeparamref name="TResult" /> 结果。</returns>
|
||||||
|
/// <exception cref="NullReferenceException">
|
||||||
|
/// 统一 CQRS runtime 返回 <see langword="null" />,但 <typeparamref name="TResult" /> 为值类型。
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="InvalidCastException">
|
||||||
|
/// 统一 CQRS runtime 返回的装箱结果无法转换为 <typeparamref name="TResult" />。
|
||||||
|
/// </exception>
|
||||||
public TResult Send<TResult>(IQuery<TResult> query)
|
public TResult Send<TResult>(IQuery<TResult> query)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
if (TryResolveDispatchContext(query, out var context))
|
var cqrsRuntime = _runtime;
|
||||||
|
|
||||||
|
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||||
{
|
{
|
||||||
var boxedResult = _runtime.SendAsync(
|
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(
|
||||||
context,
|
cqrsRuntime,
|
||||||
new LegacyQueryDispatchRequest(
|
context,
|
||||||
query,
|
new LegacyQueryDispatchRequest(
|
||||||
() => query.Do()))
|
query,
|
||||||
.AsTask()
|
() => query.Do()));
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
return (TResult)boxedResult!;
|
return (TResult)boxedResult!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.Do();
|
return query.Do();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 解析当前 legacy 查询应该绑定到哪个架构上下文。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">即将执行的 legacy 查询对象。</param>
|
|
||||||
/// <param name="context">命中时返回可用于 CQRS runtime 的架构上下文。</param>
|
|
||||||
/// <returns>如果既接入了 runtime 且查询对象提供了上下文,则返回 <see langword="true" />。</returns>
|
|
||||||
[MemberNotNullWhen(true, nameof(_runtime))]
|
|
||||||
private bool TryResolveDispatchContext(
|
|
||||||
object query,
|
|
||||||
out GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
|
||||||
{
|
|
||||||
context = null!;
|
|
||||||
|
|
||||||
if (_runtime is null || query is not IContextAware contextAware)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context = contextAware.GetContext();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,9 @@ public interface ICqrsRuntime
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />,
|
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />,
|
||||||
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
|
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
|
||||||
|
/// 为了兼容 legacy 同步入口,<c>ArchitectureContext</c>、<c>QueryExecutor</c> 与 <c>CommandExecutor</c>
|
||||||
|
/// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的
|
||||||
|
/// <see cref="SynchronizationContext" />,并应优先在内部异步链路上使用 <c>ConfigureAwait(false)</c>。
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
ValueTask<TResponse> SendAsync<TResponse>(
|
ValueTask<TResponse> SendAsync<TResponse>(
|
||||||
ICqrsContext context,
|
ICqrsContext context,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ CQRS 迁移与收敛。
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`CQRS-REWRITE-RP-094`
|
- 恢复点编号:`CQRS-REWRITE-RP-095`
|
||||||
- 当前阶段:`Phase 8`
|
- 当前阶段:`Phase 8`
|
||||||
- 当前 PR 锚点:`PR #334`
|
- 当前 PR 锚点:`PR #334`
|
||||||
- 当前结论:
|
- 当前结论:
|
||||||
@ -29,8 +29,9 @@ CQRS 迁移与收敛。
|
|||||||
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
|
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
|
||||||
- `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
|
- `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
|
||||||
- `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
|
- `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
|
||||||
- 当前 `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
|
- `RP-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
|
||||||
- `ai-plan` active 入口现以 `RP-094` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
|
- 当前 `RP-095` 已继续收口 `PR #334` 剩余 review:把 legacy 同步 bridge 的阻塞等待统一切到线程池隔离 helper、补齐 `ArchitectureContext` / executor 共享 dispatch helper、修正 bridge fixture 的并行与容器释放约束,并为 runtime bridge 与 async void command cancellation 增补回归测试
|
||||||
|
- `ai-plan` active 入口现以 `RP-095` 为最新恢复锚点;`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
|
||||||
|
|
||||||
## 当前活跃事实
|
## 当前活跃事实
|
||||||
|
|
||||||
@ -42,9 +43,13 @@ CQRS 迁移与收敛。
|
|||||||
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime`
|
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime`
|
||||||
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
|
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
|
||||||
- `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
|
- `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
|
||||||
|
- `LegacyCqrsDispatchHelper` 现统一负责 runtime dispatch context 解析,以及 legacy 同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待
|
||||||
|
- `ArchitectureContext`、`CommandExecutor`、`QueryExecutor` 的同步 CQRS/legacy bridge 入口不再直接在调用线程上阻塞 `SendAsync(...).GetAwaiter().GetResult()`
|
||||||
- `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册
|
- `GFramework.Core.Tests` 现通过 `InternalsVisibleTo("GFramework.Core.Tests")` 直接实例化内部 bridge handler,不再依赖字符串反射装配测试桥接注册
|
||||||
|
- 使用 `LegacyBridgePipelineTracker` 的 `ArchitectureContextTests` 与 `ArchitectureModulesBehaviorTests` 现都显式标记为 `NonParallelizable`
|
||||||
|
- `ArchitectureContextTests.CreateFrozenBridgeContext(...)` 现把冻结容器所有权显式交回调用方,并在每个 bridge 用例的 `finally` 中释放
|
||||||
- `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired<ICqrsRuntime>()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退
|
- `CommandExecutorModule`、`QueryExecutorModule`、`AsyncQueryExecutorModule` 现改为 `GetRequired<ICqrsRuntime>()` 并在 XML 文档里显式声明注册顺序契约,避免 runtime 缺失时静默回退
|
||||||
- `LegacyAsyncQueryDispatchRequestHandler` 与 `LegacyAsyncCommandResultDispatchRequestHandler` 现通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性
|
- `LegacyAsyncQueryDispatchRequestHandler`、`LegacyAsyncCommandResultDispatchRequestHandler`、`LegacyAsyncCommandDispatchRequestHandler` 现都通过 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)` 显式保留调用方取消可见性
|
||||||
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
|
- 相对 `ai-libs/Mediator`,当前仍未完全吸收的能力集中在六类:facade 公开入口、telemetry、stream pipeline、notification publisher 策略、生成器配置与诊断、生命周期/缓存公开配置面
|
||||||
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
|
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
|
||||||
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o <temp-dir>` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
|
- 本地 `dotnet pack GFramework.sln -c Release --no-restore -o <temp-dir>` 当前只产出 14 个预期包,未复现 benchmark `.nupkg`
|
||||||
@ -55,7 +60,7 @@ CQRS 迁移与收敛。
|
|||||||
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
|
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行
|
||||||
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
|
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
|
||||||
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
|
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
|
||||||
- `PR #334` 当前 latest-head open AI feedback 已集中到 legacy bridge / 文档收尾;本轮本地修复后,剩余 thread 应主要是待 GitHub 重新索引的状态差异或低价值建议
|
- `PR #334` 当前 latest-head open AI feedback 经过本轮本地复核与修复后,应主要剩余待 GitHub 重新索引的状态差异或已实质关闭但未 resolve 的 thread
|
||||||
|
|
||||||
## 当前风险
|
## 当前风险
|
||||||
|
|
||||||
@ -127,6 +132,13 @@ CQRS 迁移与收敛。
|
|||||||
- 结果:通过
|
- 结果:通过
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
- 结果:通过
|
- 结果:通过
|
||||||
|
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||||
|
- 结果:通过,`0 warning / 0 error`
|
||||||
|
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"`
|
||||||
|
- 结果:通过,`54/54` passed
|
||||||
|
- 备注:覆盖 legacy 同步 bridge 的同步上下文隔离、bridge fixture 容器释放,以及 async void command cancellation 可见性
|
||||||
|
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
|
||||||
|
- 结果:通过
|
||||||
|
|
||||||
## 下一推荐步骤
|
## 下一推荐步骤
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
## 2026-05-07
|
## 2026-05-07
|
||||||
|
|
||||||
|
### 阶段:PR #334 legacy bridge sync follow-up(CQRS-REWRITE-RP-095)
|
||||||
|
|
||||||
|
- 再次使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 对应的 `PR #334` latest-head review,并只保留本地复核后仍成立的问题:
|
||||||
|
- `QueryExecutor` / `CommandExecutor` 新增的同步 bridge 仍直接阻塞 `ICqrsRuntime.SendAsync(...)`,在调用方存在 `SynchronizationContext` 时容易放大 sync-over-async 死锁面
|
||||||
|
- `QueryExecutor` / `CommandExecutor` / `AsyncQueryExecutor` 各自保留一份相同的 dispatch-context 解析逻辑,仍有漂移风险
|
||||||
|
- `ArchitectureContextTests` 的 bridge fixture 依然共享静态 tracker 且未显式声明非并行;冻结容器所有权也未交还给调用方释放
|
||||||
|
- `LegacyAsyncCommandDispatchRequestHandler` 仍未沿用另两个 async bridge handler 的取消可见性模式
|
||||||
|
- 本轮主线程决策:
|
||||||
|
- 新增 `GFramework.Core/Cqrs/LegacyCqrsDispatchHelper.cs`,统一收口 legacy bridge 的 dispatch-context 解析,以及同步 bridge 对 `ICqrsRuntime.SendAsync(...)` 的线程池隔离等待
|
||||||
|
- 将 `QueryExecutor`、`CommandExecutor`、`AsyncQueryExecutor` 的重复 helper 改为复用共享 helper,并把 `ArchitectureContext` 的同步 CQRS 包装入口一并切换到同一阻塞策略,避免留下半修状态
|
||||||
|
- 为 `ICqrsRuntime.SendAsync(...)` 补充 `<remarks>`,显式说明 legacy 同步入口会在后台线程上等待该异步契约,处理链路不应依赖调用方 `SynchronizationContext`
|
||||||
|
- 把 `ArchitectureContextTests`、`ArchitectureModulesBehaviorTests` 标记为 `NonParallelizable`,并让 `CreateFrozenBridgeContext(...)` 把冻结容器通过 `out` 参数返还给每个测试在 `finally` 中释放
|
||||||
|
- 为 `LegacyAsyncCommandDispatchRequestHandler` 增补 `ThrowIfCancellationRequested()` + `WaitAsync(cancellationToken)`,与另外两个 async bridge handler 保持一致
|
||||||
|
- 新增回归测试覆盖同步 bridge 的 `SynchronizationContext` 隔离、legacy async command handler 的取消语义,以及 async/sync bridge request 的 request-type 命中
|
||||||
|
- 本轮权威验证:
|
||||||
|
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
|
||||||
|
- 结果:通过
|
||||||
|
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||||
|
- 结果:通过,`0 warning / 0 error`
|
||||||
|
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests|FullyQualifiedName~ArchitectureModulesBehaviorTests|FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AsyncQueryExecutorTests|FullyQualifiedName~LegacyAsyncCommandDispatchRequestHandlerTests"`
|
||||||
|
- 结果:通过,`54/54` passed
|
||||||
|
|
||||||
### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094)
|
### 阶段:PR #334 legacy bridge / 文档 review 收尾(CQRS-REWRITE-RP-094)
|
||||||
|
|
||||||
- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`
|
- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user