From 8990749d910d6c27af8059d03c8b91a7146cf15a Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 11 May 2026 12:39:37 +0800
Subject: [PATCH] =?UTF-8?q?test(cqrs-benchmarks):=20=E8=A1=A5=E9=BD=90=20R?=
=?UTF-8?q?equestStartup=20=E7=9A=84=20Mediator=20=E5=AF=B9=E7=85=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 RequestStartupBenchmarks 的 Mediator 初始化与冷启动 benchmark 路径
- 更新 request 与 handler 契约以复用单文件内的 Mediator concrete host 对照
---
.../Messaging/RequestStartupBenchmarks.cs | 53 ++-
.../Cqrs/CqrsDispatcherCacheTests.cs | 326 +++++++++++++++++-
2 files changed, 365 insertions(+), 14 deletions(-)
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs
index b37fa20c..2c685fed 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs
@@ -17,11 +17,12 @@ using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using ILogger = GFramework.Core.Abstractions.Logging.ILogger;
+using GeneratedMediator = Mediator.Mediator;
namespace GFramework.Cqrs.Benchmarks.Messaging;
///
-/// 对比 request 宿主的初始化与首次分发成本,作为后续吸收 `Mediator` comparison benchmark 设计的 startup 基线。
+/// 对比 request 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次分发成本。
///
[Config(typeof(Config))]
public class RequestStartupBenchmarks
@@ -31,7 +32,9 @@ public class RequestStartupBenchmarks
private MicrosoftDiContainer _container = null!;
private ServiceProvider _serviceProvider = null!;
+ private ServiceProvider _mediatorServiceProvider = null!;
private IMediator _mediatr = null!;
+ private GeneratedMediator _mediator = null!;
private ICqrsRuntime _runtime = null!;
///
@@ -63,6 +66,8 @@ public class RequestStartupBenchmarks
_serviceProvider = CreateMediatRServiceProvider();
_mediatr = _serviceProvider.GetRequiredService();
+ _mediatorServiceProvider = CreateMediatorServiceProvider();
+ _mediator = _mediatorServiceProvider.GetRequiredService();
_container = CreateGFrameworkContainer();
_runtime = CreateGFrameworkRuntime(_container);
}
@@ -86,7 +91,7 @@ public class RequestStartupBenchmarks
[GlobalCleanup]
public void Cleanup()
{
- BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
+ BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider, _mediatorServiceProvider);
}
///
@@ -109,6 +114,16 @@ public class RequestStartupBenchmarks
return _runtime;
}
+ ///
+ /// 返回已构建宿主中的 `Mediator` concrete mediator,作为 source-generated 对照组的初始化句柄。
+ ///
+ [Benchmark]
+ [BenchmarkCategory("Initialization")]
+ public GeneratedMediator Initialization_Mediator()
+ {
+ return _mediator;
+ }
+
///
/// 在新宿主上首次发送 request,作为 MediatR 的 cold-start baseline。
///
@@ -133,6 +148,18 @@ public class RequestStartupBenchmarks
return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false);
}
+ ///
+ /// 在新的 `Mediator` 宿主上首次发送 request,量化 source-generated concrete path 的 cold-start 成本。
+ ///
+ [Benchmark]
+ [BenchmarkCategory("ColdStart")]
+ public async ValueTask ColdStart_Mediator()
+ {
+ using var serviceProvider = CreateMediatorServiceProvider();
+ var mediator = serviceProvider.GetRequiredService();
+ return await mediator.Send(Request, CancellationToken.None).ConfigureAwait(false);
+ }
+
///
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
///
@@ -170,6 +197,14 @@ public class RequestStartupBenchmarks
ServiceLifetime.Transient);
}
+ ///
+ /// 构建只承载当前 benchmark request 的最小 `Mediator` 对照宿主。
+ ///
+ private static ServiceProvider CreateMediatorServiceProvider()
+ {
+ return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
+ }
+
///
/// 为 benchmark 创建稳定的 fatal 级 logger,避免把日志成本混入 startup 测量。
///
@@ -188,6 +223,7 @@ public class RequestStartupBenchmarks
/// 请求标识。
public sealed record BenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest,
+ Mediator.IRequest,
MediatR.IRequest;
///
@@ -197,10 +233,11 @@ public class RequestStartupBenchmarks
public sealed record BenchmarkResponse(Guid Id);
///
- /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
+ /// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。
///
public sealed class BenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler,
+ Mediator.IRequestHandler,
MediatR.IRequestHandler
{
///
@@ -211,6 +248,16 @@ public class RequestStartupBenchmarks
return ValueTask.FromResult(new BenchmarkResponse(request.Id));
}
+ ///
+ /// 处理 NuGet `Mediator` request。
+ ///
+ ValueTask Mediator.IRequestHandler.Handle(
+ BenchmarkRequest request,
+ CancellationToken cancellationToken)
+ {
+ return Handle(request, cancellationToken);
+ }
+
///
/// 处理 MediatR request。
///
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs
index 0a71dac9..952d31b9 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs
@@ -309,6 +309,60 @@ internal sealed class CqrsDispatcherCacheTests
});
}
+ ///
+ /// 验证同一 request dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor,
+ /// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
+ ///
+ [Test]
+ public async Task Dispatcher_Should_Create_Request_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
+ {
+ var requestBindings = GetCacheField("RequestDispatchBindings");
+ var behaviorType = typeof(IPipelineBehavior);
+ using var zeroBehaviorContainer = CreateFrozenContainer(
+ new DispatcherCacheFixtureOptions
+ {
+ IncludeRequestPipelineCacheBehavior = false
+ });
+ var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
+ var behaviorContext = new ArchitectureContext(_container!);
+ var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext);
+ var behaviorDispatcher = GetDispatcherFromContext(behaviorContext);
+
+ await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
+
+ var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
+ requestBindings,
+ typeof(DispatcherPipelineCacheRequest),
+ typeof(int));
+ var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue(
+ requestBindings,
+ typeof(DispatcherPipelineCacheRequest),
+ typeof(int),
+ 1);
+
+ await behaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
+
+ var bindingAfterBehaviorDispatch = GetPairCacheValue(
+ requestBindings,
+ typeof(DispatcherPipelineCacheRequest),
+ typeof(int));
+ var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue(
+ requestBindings,
+ typeof(DispatcherPipelineCacheRequest),
+ typeof(int),
+ 1);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
+ Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
+ Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
+ Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
+ AssertRequestBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false);
+ AssertRequestBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true);
+ });
+ }
+
///
/// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。
///
@@ -374,6 +428,60 @@ internal sealed class CqrsDispatcherCacheTests
});
}
+ ///
+ /// 验证同一 stream dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor,
+ /// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
+ ///
+ [Test]
+ public async Task Dispatcher_Should_Create_Stream_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
+ {
+ var streamBindings = GetCacheField("StreamDispatchBindings");
+ var behaviorType = typeof(IStreamPipelineBehavior);
+ using var zeroBehaviorContainer = CreateFrozenContainer(
+ new DispatcherCacheFixtureOptions
+ {
+ IncludeStreamPipelineCacheBehavior = false
+ });
+ var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
+ var behaviorContext = new ArchitectureContext(_container!);
+ var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext);
+ var behaviorDispatcher = GetDispatcherFromContext(behaviorContext);
+
+ await DrainAsync(zeroBehaviorContext.CreateStream(new DispatcherCacheStreamRequest()));
+
+ var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
+ streamBindings,
+ typeof(DispatcherCacheStreamRequest),
+ typeof(int));
+ var executorAfterZeroBehaviorDispatch = GetStreamPipelineExecutorValue(
+ streamBindings,
+ typeof(DispatcherCacheStreamRequest),
+ typeof(int),
+ 1);
+
+ await DrainAsync(behaviorContext.CreateStream(new DispatcherCacheStreamRequest()));
+
+ var bindingAfterBehaviorDispatch = GetPairCacheValue(
+ streamBindings,
+ typeof(DispatcherCacheStreamRequest),
+ typeof(int));
+ var executorAfterBehaviorDispatch = GetStreamPipelineExecutorValue(
+ streamBindings,
+ typeof(DispatcherCacheStreamRequest),
+ typeof(int),
+ 1);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
+ Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
+ Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
+ Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
+ AssertStreamBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false);
+ AssertStreamBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true);
+ });
+ }
+
///
/// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。
///
@@ -492,6 +600,71 @@ internal sealed class CqrsDispatcherCacheTests
});
}
+ ///
+ /// 验证同一 request dispatch binding 先在零行为 dispatcher 上命中后,
+ /// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler,并为当前上下文重新注入架构实例。
+ ///
+ [Test]
+ public async Task Dispatcher_Should_Reinject_Current_Request_Context_When_Shared_Binding_Switches_From_Zero_Pipeline()
+ {
+ DispatcherPipelineContextRefreshState.Reset();
+
+ var requestBindings = GetCacheField("RequestDispatchBindings");
+ using var zeroBehaviorContainer = CreateFrozenContainer(
+ new DispatcherCacheFixtureOptions
+ {
+ IncludeRequestPipelineContextRefreshBehavior = false
+ });
+ var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
+ var behaviorContext = new ArchitectureContext(_container!);
+
+ await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("without-behavior"));
+
+ var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
+ requestBindings,
+ typeof(DispatcherPipelineContextRefreshRequest),
+ typeof(int));
+ var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue(
+ requestBindings,
+ typeof(DispatcherPipelineContextRefreshRequest),
+ typeof(int),
+ 1);
+
+ await behaviorContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("with-behavior"));
+
+ var bindingAfterBehaviorDispatch = GetPairCacheValue(
+ requestBindings,
+ typeof(DispatcherPipelineContextRefreshRequest),
+ typeof(int));
+ var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue(
+ requestBindings,
+ typeof(DispatcherPipelineContextRefreshRequest),
+ typeof(int),
+ 1);
+ var behaviorSnapshots = DispatcherPipelineContextRefreshState.BehaviorSnapshots.ToArray();
+ var handlerSnapshots = DispatcherPipelineContextRefreshState.HandlerSnapshots.ToArray();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
+ Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
+ Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
+ Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
+
+ Assert.That(behaviorSnapshots, Has.Length.EqualTo(1));
+ Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("with-behavior"));
+ Assert.That(behaviorSnapshots[0].Context, Is.SameAs(behaviorContext));
+
+ Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
+ Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("without-behavior"));
+ Assert.That(handlerSnapshots[0].Context, Is.SameAs(zeroBehaviorContext));
+ Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("with-behavior"));
+ Assert.That(handlerSnapshots[1].Context, Is.SameAs(behaviorContext));
+ Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
+ Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
+ });
+ }
+
///
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler,
/// 并为当次实例重新注入当前架构上下文。
@@ -632,6 +805,108 @@ internal sealed class CqrsDispatcherCacheTests
});
}
+ ///
+ /// 验证同一 stream dispatch binding 先在零行为 dispatcher 上命中后,
+ /// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler,并为当前上下文重新注入架构实例。
+ ///
+ [Test]
+ public async Task Dispatcher_Should_Reinject_Current_Stream_Context_When_Shared_Binding_Switches_From_Zero_Pipeline()
+ {
+ DispatcherStreamContextRefreshState.Reset();
+
+ var streamBindings = GetCacheField("StreamDispatchBindings");
+ using var zeroBehaviorContainer = CreateFrozenContainer(
+ new DispatcherCacheFixtureOptions
+ {
+ IncludeStreamPipelineContextRefreshBehavior = false
+ });
+ var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
+ var behaviorContext = new ArchitectureContext(_container!);
+
+ await DrainAsync(zeroBehaviorContext.CreateStream(new DispatcherStreamContextRefreshRequest("without-behavior")));
+
+ var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
+ streamBindings,
+ typeof(DispatcherStreamContextRefreshRequest),
+ typeof(int));
+ var executorAfterZeroBehaviorDispatch = GetStreamPipelineExecutorValue(
+ streamBindings,
+ typeof(DispatcherStreamContextRefreshRequest),
+ typeof(int),
+ 1);
+
+ await DrainAsync(behaviorContext.CreateStream(new DispatcherStreamContextRefreshRequest("with-behavior")));
+
+ var bindingAfterBehaviorDispatch = GetPairCacheValue(
+ streamBindings,
+ typeof(DispatcherStreamContextRefreshRequest),
+ typeof(int));
+ var executorAfterBehaviorDispatch = GetStreamPipelineExecutorValue(
+ streamBindings,
+ typeof(DispatcherStreamContextRefreshRequest),
+ typeof(int),
+ 1);
+ var behaviorSnapshots = DispatcherStreamContextRefreshState.BehaviorSnapshots.ToArray();
+ var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
+ Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
+ Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
+ Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
+
+ Assert.That(behaviorSnapshots, Has.Length.EqualTo(1));
+ Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("with-behavior"));
+ Assert.That(behaviorSnapshots[0].Context, Is.SameAs(behaviorContext));
+
+ Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
+ Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("without-behavior"));
+ Assert.That(handlerSnapshots[0].Context, Is.SameAs(zeroBehaviorContext));
+ Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("with-behavior"));
+ Assert.That(handlerSnapshots[1].Context, Is.SameAs(behaviorContext));
+ Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
+ Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
+ });
+ }
+
+ ///
+ /// 描述缓存测试 fixture 需要启用的可选 pipeline 行为集合,
+ /// 用于构造“同一静态 binding 对应不同 dispatcher 注册可见性”的组合场景。
+ ///
+ private sealed class DispatcherCacheFixtureOptions
+ {
+ ///
+ /// 获取是否注册 。
+ ///
+ public bool IncludeRequestPipelineCacheBehavior { get; init; } = true;
+
+ ///
+ /// 获取是否注册 。
+ ///
+ public bool IncludeRequestPipelineContextRefreshBehavior { get; init; } = true;
+
+ ///
+ /// 获取是否注册 request 顺序验证所需的两层 pipeline 行为。
+ ///
+ public bool IncludeRequestPipelineOrderBehaviors { get; init; } = true;
+
+ ///
+ /// 获取是否注册 。
+ ///
+ public bool IncludeStreamPipelineCacheBehavior { get; init; } = true;
+
+ ///
+ /// 获取是否注册 。
+ ///
+ public bool IncludeStreamPipelineContextRefreshBehavior { get; init; } = true;
+
+ ///
+ /// 获取是否注册 stream 顺序验证所需的两层 pipeline 行为。
+ ///
+ public bool IncludeStreamPipelineOrderBehaviors { get; init; } = true;
+ }
+
///
/// 通过反射读取 dispatcher 的静态缓存对象。
///
@@ -679,10 +954,11 @@ internal sealed class CqrsDispatcherCacheTests
/// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器,
/// 用于验证 dispatcher 的实例级缓存不会跨容器共享。
///
- private static MicrosoftDiContainer CreateFrozenContainer()
+ /// 控制当前隔离容器要启用哪些可选 pipeline 行为的配置。
+ private static MicrosoftDiContainer CreateFrozenContainer(DispatcherCacheFixtureOptions? options = null)
{
var container = new MicrosoftDiContainer();
- ConfigureDispatcherCacheFixture(container);
+ ConfigureDispatcherCacheFixture(container, options);
container.Freeze();
return container;
@@ -692,16 +968,44 @@ internal sealed class CqrsDispatcherCacheTests
/// 组装当前 fixture 依赖的 CQRS 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。
///
/// 待补齐 CQRS 注册的目标容器。
- private static void ConfigureDispatcherCacheFixture(MicrosoftDiContainer container)
+ /// 控制是否跳过特定 pipeline 行为注册的可选配置。
+ private static void ConfigureDispatcherCacheFixture(
+ MicrosoftDiContainer container,
+ DispatcherCacheFixtureOptions? options = null)
{
- container.RegisterCqrsPipelineBehavior();
- container.RegisterCqrsPipelineBehavior();
- container.RegisterCqrsPipelineBehavior();
- container.RegisterCqrsPipelineBehavior();
- container.RegisterCqrsStreamPipelineBehavior();
- container.RegisterCqrsStreamPipelineBehavior();
- container.RegisterCqrsStreamPipelineBehavior();
- container.RegisterCqrsStreamPipelineBehavior();
+ options ??= new DispatcherCacheFixtureOptions();
+
+ if (options.IncludeRequestPipelineCacheBehavior)
+ {
+ container.RegisterCqrsPipelineBehavior();
+ }
+
+ if (options.IncludeRequestPipelineContextRefreshBehavior)
+ {
+ container.RegisterCqrsPipelineBehavior();
+ }
+
+ if (options.IncludeRequestPipelineOrderBehaviors)
+ {
+ container.RegisterCqrsPipelineBehavior();
+ container.RegisterCqrsPipelineBehavior();
+ }
+
+ if (options.IncludeStreamPipelineCacheBehavior)
+ {
+ container.RegisterCqrsStreamPipelineBehavior();
+ }
+
+ if (options.IncludeStreamPipelineContextRefreshBehavior)
+ {
+ container.RegisterCqrsStreamPipelineBehavior();
+ }
+
+ if (options.IncludeStreamPipelineOrderBehaviors)
+ {
+ container.RegisterCqrsStreamPipelineBehavior();
+ container.RegisterCqrsStreamPipelineBehavior();
+ }
CqrsTestRuntime.RegisterHandlers(
container,