mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-08 17:44:29 +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方法 - 获取未注册工具时抛出异常
|
||||
/// - GetEnvironment方法 - 获取环境对象
|
||||
/// </summary>
|
||||
[NonParallelizable]
|
||||
[TestFixture]
|
||||
public class ArchitectureContextTests
|
||||
{
|
||||
@ -150,13 +151,20 @@ public class ArchitectureContextTests
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
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(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
Assert.That(result, Is.EqualTo(24));
|
||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -190,13 +198,20 @@ public class ArchitectureContextTests
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
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.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
Assert.That(testCommand.Executed, Is.True);
|
||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -230,13 +245,20 @@ public class ArchitectureContextTests
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
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(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
Assert.That(testCommand.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -247,22 +269,30 @@ public class ArchitectureContextTests
|
||||
{
|
||||
LegacyBridgePipelineTracker.Reset();
|
||||
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(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
Assert.That(result, Is.EqualTo(64));
|
||||
Assert.That(testQuery.ObservedContext, Is.SameAs(bridgeContext));
|
||||
Assert.That(LegacyBridgePipelineTracker.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bridgeContainer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为需要验证统一 CQRS pipeline 的用例创建一个已冻结的最小 bridge 上下文。
|
||||
/// </summary>
|
||||
/// <param name="container">返回承载当前 bridge 上下文的冻结容器,供测试在 finally 中显式释放。</param>
|
||||
/// <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);
|
||||
new CqrsRuntimeModule().Register(container);
|
||||
container.ExecuteServicesHook(services =>
|
||||
|
||||
@ -15,6 +15,7 @@ namespace GFramework.Core.Tests.Architectures;
|
||||
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
|
||||
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
|
||||
/// </summary>
|
||||
[NonParallelizable]
|
||||
[TestFixture]
|
||||
public class ArchitectureModulesBehaviorTests
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Core.Command;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
|
||||
namespace GFramework.Core.Tests.Command;
|
||||
|
||||
@ -75,6 +76,59 @@ public class CommandExecutorTests
|
||||
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>
|
||||
/// 测试SendAsync方法执行异步命令
|
||||
/// </summary>
|
||||
@ -122,4 +176,11 @@ public class CommandExecutorTests
|
||||
{
|
||||
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
|
||||
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
using GFramework.Core.Tests.Command;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
@ -138,4 +140,32 @@ public class AsyncQueryExecutorTests
|
||||
Assert.That(result1, Is.EqualTo(20));
|
||||
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
|
||||
|
||||
using GFramework.Core.Query;
|
||||
using GFramework.Core.Tests.Architectures;
|
||||
using GFramework.Core.Tests.Command;
|
||||
|
||||
namespace GFramework.Core.Tests.Query;
|
||||
|
||||
@ -61,4 +63,44 @@ public class QueryExecutorTests
|
||||
|
||||
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>
|
||||
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
|
||||
{
|
||||
return SendRequestAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return LegacyCqrsDispatchHelper.SendSynchronously(CqrsRuntime, this, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -197,7 +198,8 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <returns>查询结果</returns>
|
||||
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>
|
||||
@ -403,7 +405,8 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <returns>命令执行结果</returns>
|
||||
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>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using IAsyncCommand = GFramework.Core.Abstractions.Command.IAsyncCommand;
|
||||
@ -78,9 +76,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
||||
{
|
||||
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();
|
||||
@ -97,9 +97,11 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
||||
{
|
||||
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();
|
||||
@ -119,12 +121,14 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
||||
where TTarget : class
|
||||
where TRequest : IRequest<Unit>
|
||||
{
|
||||
if (!TryResolveDispatchContext(target, out var context))
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
|
||||
LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -145,13 +149,15 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
||||
where TTarget : class
|
||||
where TRequest : IRequest<object?>
|
||||
{
|
||||
if (!TryResolveDispatchContext(target, out var context))
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (!LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, target, out var context))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var boxedResult = _runtime.SendAsync(context, requestFactory(target)).AsTask().GetAwaiter().GetResult();
|
||||
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(cqrsRuntime, context, requestFactory(target));
|
||||
result = (TResult)boxedResult!;
|
||||
return true;
|
||||
}
|
||||
@ -177,33 +183,4 @@ public sealed class CommandExecutor(ICqrsRuntime? runtime = null) : ICommandExec
|
||||
.ConfigureAwait(false);
|
||||
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)
|
||||
{
|
||||
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);
|
||||
await request.Command.ExecuteAsync().ConfigureAwait(false);
|
||||
await request.Command.ExecuteAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
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
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
@ -31,9 +29,11 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
|
||||
{
|
||||
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();
|
||||
@ -61,32 +61,4 @@ public sealed class AsyncQueryExecutor(ICqrsRuntime? runtime = null) : IAsyncQue
|
||||
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
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Core.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
@ -31,53 +29,30 @@ public sealed class QueryExecutor(ICqrsRuntime? runtime = null) : IQueryExecutor
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">查询结果的类型。</typeparam>
|
||||
/// <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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
if (TryResolveDispatchContext(query, out var context))
|
||||
var cqrsRuntime = _runtime;
|
||||
|
||||
if (LegacyCqrsDispatchHelper.TryResolveDispatchContext(cqrsRuntime, query, out var context))
|
||||
{
|
||||
var boxedResult = _runtime.SendAsync(
|
||||
context,
|
||||
new LegacyQueryDispatchRequest(
|
||||
query,
|
||||
() => query.Do()))
|
||||
.AsTask()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
var boxedResult = LegacyCqrsDispatchHelper.SendSynchronously(
|
||||
cqrsRuntime,
|
||||
context,
|
||||
new LegacyQueryDispatchRequest(
|
||||
query,
|
||||
() => query.Do()));
|
||||
return (TResult)boxedResult!;
|
||||
}
|
||||
|
||||
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>
|
||||
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />,
|
||||
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
|
||||
/// 为了兼容 legacy 同步入口,<c>ArchitectureContext</c>、<c>QueryExecutor</c> 与 <c>CommandExecutor</c>
|
||||
/// 可能会在后台线程上同步等待该异步结果;实现者与 pipeline 行为不应依赖调用方的
|
||||
/// <see cref="SynchronizationContext" />,并应优先在内部异步链路上使用 <c>ConfigureAwait(false)</c>。
|
||||
/// </remarks>
|
||||
ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
|
||||
@ -7,7 +7,7 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-094`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-095`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前 PR 锚点:`PR #334`
|
||||
- 当前结论:
|
||||
@ -29,8 +29,9 @@ CQRS 迁移与收敛。
|
||||
- 当前 `RP-091` 已把 benchmark 项目发布面隔离与包清单校验前移到 PR:`GFramework.Cqrs.Benchmarks` 明确保持不可打包,`publish.yml` 与 `ci.yml` 复用同一份 packed-modules 校验脚本
|
||||
- `RP-092` 已补齐 request handler `Singleton / Transient` 生命周期矩阵 benchmark,并明确把 `Scoped` 对照留到具备真实显式作用域边界的宿主模型后再评估
|
||||
- `RP-093` 已把 `GFramework.Core` 的 legacy `SendCommand` / `SendQuery` 兼容入口收敛到底层统一 `GFramework.Cqrs` runtime,同时补充 `Mediator` 未吸收能力差距复核
|
||||
- 当前 `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-094` 已按 `PR #334` latest-head review 收口 legacy bridge 的测试注册方式、模块运行时依赖契约、异步取消语义、XML 文档缺口与兼容文档回退边界
|
||||
- 当前 `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`
|
||||
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
|
||||
- `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,不再依赖字符串反射装配测试桥接注册
|
||||
- 使用 `LegacyBridgePipelineTracker` 的 `ArchitectureContextTests` 与 `ArchitectureModulesBehaviorTests` 现都显式标记为 `NonParallelizable`
|
||||
- `ArchitectureContextTests.CreateFrozenBridgeContext(...)` 现把冻结容器所有权显式交回调用方,并在每个 bridge 用例的 `finally` 中释放
|
||||
- `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 策略、生成器配置与诊断、生命周期/缓存公开配置面
|
||||
- 发布工作流已有 packed modules 校验,但 PR 工作流此前没有等价的 solution pack 产物名单校验
|
||||
- 本地 `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 输入直接插值到命令行
|
||||
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
|
||||
- `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`
|
||||
- 结果:通过
|
||||
- `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
|
||||
|
||||
### 阶段: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)
|
||||
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支公开 PR,确认 `feat/cqrs-optimization` 当前对应 `PR #334`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user