Merge pull request #348 from GeWuYou/feat/cqrs-optimization

Feat/Enhance CQRS benchmarks coverage and generated invoker descriptor validation
This commit is contained in:
gewuyou 2026-05-11 17:33:43 +08:00 committed by GitHub
commit ef4d3d5ddf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 2808 additions and 119 deletions

View File

@ -0,0 +1,321 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using System;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 对比单处理器 notification publish 在不同 handler 生命周期下的额外开销。
/// </summary>
/// <remarks>
/// 当前矩阵覆盖 <c>Singleton</c>、<c>Scoped</c> 与 <c>Transient</c>。
/// 其中 <c>Scoped</c> 会在每次 notification publish 前显式创建并释放真实的 DI 作用域,
/// 避免把 scoped handler 错误地压到根容器解析而扭曲生命周期对照。
/// </remarks>
[Config(typeof(Config))]
public class NotificationLifetimeBenchmarks
{
private MicrosoftDiContainer _container = null!;
private ICqrsRuntime? _runtime;
private ScopedBenchmarkContainer? _scopedContainer;
private ICqrsRuntime? _scopedRuntime;
private ServiceProvider _serviceProvider = null!;
private IPublisher? _publisher;
private BenchmarkNotificationHandler _baselineHandler = null!;
private BenchmarkNotification _notification = null!;
private ILogger _runtimeLogger = null!;
/// <summary>
/// 控制当前 benchmark 使用的 handler 生命周期。
/// </summary>
[Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
public HandlerLifetime Lifetime { get; set; }
/// <summary>
/// 可公平比较的 benchmark handler 生命周期集合。
/// </summary>
public enum HandlerLifetime
{
/// <summary>
/// 复用单个 handler 实例。
/// </summary>
Singleton,
/// <summary>
/// 每次 publish 在显式作用域内解析并复用 handler 实例。
/// </summary>
Scoped,
/// <summary>
/// 每次 publish 都重新解析新的 handler 实例。
/// </summary>
Transient
}
/// <summary>
/// 配置 notification lifetime benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationLifetime"));
AddDiagnoser(MemoryDiagnoser.Default);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建当前生命周期下的 GFramework 与 MediatR notification 对照宿主。
/// </summary>
[GlobalSetup]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
Fixture.Setup($"NotificationLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkNotificationHandler();
_notification = new BenchmarkNotification(Guid.NewGuid());
_runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationLifetimeBenchmarks) + "." + Lifetime);
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
RegisterGFrameworkHandler(container, Lifetime);
});
if (Lifetime != HandlerLifetime.Scoped)
{
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(_container, _runtimeLogger);
}
else
{
_scopedContainer = new ScopedBenchmarkContainer(_container);
_scopedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(_scopedContainer, _runtimeLogger);
}
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(NotificationLifetimeBenchmarks),
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
ResolveMediatRLifetime(Lifetime));
if (Lifetime != HandlerLifetime.Scoped)
{
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
}
}
/// <summary>
/// 释放当前生命周期矩阵持有的 benchmark 宿主资源。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
try
{
BenchmarkCleanupHelper.DisposeAll(_scopedContainer, _container, _serviceProvider);
}
finally
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
}
/// <summary>
/// 直接调用 handler作为不同生命周期矩阵下的 publish 额外开销 baseline。
/// </summary>
/// <returns>代表基线 handler 完成当前 notification 处理的值任务。</returns>
[Benchmark(Baseline = true)]
public ValueTask PublishNotification_Baseline()
{
return _baselineHandler.Handle(_notification, CancellationToken.None);
}
/// <summary>
/// 通过 GFramework.CQRS runtime 发布 notification。
/// </summary>
/// <returns>代表当前 GFramework.CQRS publish 完成的值任务。</returns>
[Benchmark]
public ValueTask PublishNotification_GFrameworkCqrs()
{
if (Lifetime == HandlerLifetime.Scoped)
{
return PublishScopedGFrameworkNotificationAsync(
_scopedRuntime!,
_scopedContainer!,
_notification,
CancellationToken.None);
}
return _runtime!.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
}
/// <summary>
/// 通过 MediatR 发布 notification作为外部对照。
/// </summary>
/// <returns>代表当前 MediatR publish 完成的任务。</returns>
[Benchmark]
public Task PublishNotification_MediatR()
{
if (Lifetime == HandlerLifetime.Scoped)
{
return PublishScopedMediatRNotificationAsync(_serviceProvider, _notification, CancellationToken.None);
}
return _publisher!.Publish(_notification, CancellationToken.None);
}
/// <summary>
/// 按生命周期把 benchmark notification handler 注册到 GFramework 容器。
/// </summary>
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
/// <param name="lifetime">待比较的 handler 生命周期。</param>
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
{
ArgumentNullException.ThrowIfNull(container);
switch (lifetime)
{
case HandlerLifetime.Singleton:
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
return;
case HandlerLifetime.Scoped:
container.RegisterScoped<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
return;
case HandlerLifetime.Transient:
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
return;
default:
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
}
}
/// <summary>
/// 将 benchmark 生命周期映射为 MediatR 组装所需的 <see cref="ServiceLifetime" />。
/// </summary>
/// <param name="lifetime">待比较的 handler 生命周期。</param>
private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
{
return lifetime switch
{
HandlerLifetime.Singleton => ServiceLifetime.Singleton,
HandlerLifetime.Scoped => ServiceLifetime.Scoped,
HandlerLifetime.Transient => ServiceLifetime.Transient,
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
};
}
/// <summary>
/// 在真实的 publish 级作用域内执行一次 GFramework.CQRS notification 分发。
/// </summary>
/// <param name="runtime">复用的 scoped benchmark runtime。</param>
/// <param name="scopedContainer">负责为每次 publish 激活独立作用域的只读容器适配层。</param>
/// <param name="notification">要发布的 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>代表当前 publish 完成的值任务。</returns>
/// <remarks>
/// notification lifetime benchmark 只关心 handler 解析和 publish 本身的热路径,
/// 因此这里复用同一个 runtime但在每次调用前后显式创建并释放新的 DI 作用域,
/// 让 scoped handler 真正绑定到 publish 边界。
/// </remarks>
private static async ValueTask PublishScopedGFrameworkNotificationAsync(
ICqrsRuntime runtime,
ScopedBenchmarkContainer scopedContainer,
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(scopedContainer);
ArgumentNullException.ThrowIfNull(notification);
using var scopeLease = scopedContainer.EnterScope();
await runtime.PublishAsync(BenchmarkContext.Instance, notification, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 在真实的 publish 级作用域内执行一次 MediatR notification 分发。
/// </summary>
/// <param name="rootServiceProvider">当前 benchmark 的根 <see cref="ServiceProvider" />。</param>
/// <param name="notification">要发布的 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>代表当前 publish 完成的任务。</returns>
/// <remarks>
/// 这里显式从新的 scope 解析 <see cref="IPublisher" />,确保 <c>Scoped</c> handler 与依赖绑定到 publish 边界。
/// </remarks>
private static async Task PublishScopedMediatRNotificationAsync(
ServiceProvider rootServiceProvider,
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(rootServiceProvider);
ArgumentNullException.ThrowIfNull(notification);
using var scope = rootServiceProvider.CreateScope();
var publisher = scope.ServiceProvider.GetRequiredService<IPublisher>();
await publisher.Publish(notification, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Benchmark notification。
/// </summary>
/// <param name="Id">通知标识。</param>
public sealed record BenchmarkNotification(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.INotification,
MediatR.INotification;
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler :
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
/// <param name="notification">当前要处理的 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>代表当前 notification 处理完成的值任务。</returns>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(notification);
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.CompletedTask;
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(notification);
cancellationToken.ThrowIfCancellationRequested();
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,289 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using System;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
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;
/// <summary>
/// 对比 notification 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次发布成本。
/// </summary>
/// <remarks>
/// 该矩阵刻意保持“单 notification + 单 handler + 最小宿主”的对称形状,
/// 只观察宿主构建与首个 publish 命中的额外开销,不把 fan-out 或自定义发布策略混入 startup 结论。
/// </remarks>
[Config(typeof(Config))]
public class NotificationStartupBenchmarks
{
private static readonly ILogger RuntimeLogger = CreateLogger(nameof(NotificationStartupBenchmarks));
private static readonly BenchmarkNotification Notification = new(Guid.NewGuid());
private MicrosoftDiContainer _container = null!;
private ICqrsRuntime _runtime = null!;
private ServiceProvider _serviceProvider = null!;
private IPublisher _publisher = null!;
private ServiceProvider _mediatorServiceProvider = null!;
private GeneratedMediator _mediator = null!;
/// <summary>
/// 配置 notification startup benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default
.WithId("ColdStart")
.WithInvocationCount(1)
.WithUnrollFactor(1));
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "NotificationStartup"), TargetMethodColumn.Method, CategoriesColumn.Default);
AddDiagnoser(MemoryDiagnoser.Default);
AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByCategory);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建 startup benchmark 复用的最小 notification 宿主对象。
/// </summary>
[GlobalSetup]
public void Setup()
{
Fixture.Setup("NotificationStartup", handlerCount: 1, pipelineCount: 0);
_serviceProvider = CreateMediatRServiceProvider();
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
_mediatorServiceProvider = CreateMediatorServiceProvider();
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
_container = CreateGFrameworkContainer();
_runtime = CreateGFrameworkRuntime(_container);
}
/// <summary>
/// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保每组 benchmark 都重新命中首次绑定路径。
/// </summary>
[IterationSetup]
public void ResetColdStartCaches()
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
/// <summary>
/// 释放 startup benchmark 复用的宿主对象。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider, _mediatorServiceProvider);
}
/// <summary>
/// 返回已构建宿主中的 MediatR publisher作为 initialization 组的句柄解析 baseline。
/// </summary>
/// <returns>当前 benchmark 复用的 MediatR publisher。</returns>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Initialization")]
public IPublisher Initialization_MediatR()
{
return _publisher;
}
/// <summary>
/// 返回已构建宿主中的 GFramework.CQRS runtime确保与 MediatR baseline 处于相同初始化阶段。
/// </summary>
/// <returns>当前 benchmark 复用的 GFramework.CQRS runtime。</returns>
[Benchmark]
[BenchmarkCategory("Initialization")]
public ICqrsRuntime Initialization_GFrameworkCqrs()
{
return _runtime;
}
/// <summary>
/// 返回已构建宿主中的 `Mediator` concrete mediator作为 source-generated 对照组的初始化句柄。
/// </summary>
/// <returns>当前 benchmark 复用的 `Mediator` concrete mediator。</returns>
[Benchmark]
[BenchmarkCategory("Initialization")]
public GeneratedMediator Initialization_Mediator()
{
return _mediator;
}
/// <summary>
/// 在新宿主上首次发布 notification作为 MediatR 的 cold-start baseline。
/// </summary>
/// <returns>代表首次 publish 完成的任务。</returns>
[Benchmark(Baseline = true)]
[BenchmarkCategory("ColdStart")]
public async Task ColdStart_MediatR()
{
using var serviceProvider = CreateMediatRServiceProvider();
var publisher = serviceProvider.GetRequiredService<IPublisher>();
await publisher.Publish(Notification, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 在新 runtime 上首次发布 notification量化 GFramework.CQRS 的 first-hit 成本。
/// </summary>
/// <returns>代表首次 publish 完成的值任务。</returns>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public async ValueTask ColdStart_GFrameworkCqrs()
{
using var container = CreateGFrameworkContainer();
var runtime = CreateGFrameworkRuntime(container);
await runtime.PublishAsync(BenchmarkContext.Instance, Notification, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 在新的 `Mediator` 宿主上首次发布 notification量化 source-generated concrete path 的 cold-start 成本。
/// </summary>
/// <returns>代表首次 publish 完成的值任务。</returns>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public async ValueTask ColdStart_Mediator()
{
using var serviceProvider = CreateMediatorServiceProvider();
var mediator = serviceProvider.GetRequiredService<GeneratedMediator>();
await mediator.Publish(Notification, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 构建只承载当前 benchmark notification 的最小 GFramework.CQRS runtime。
/// </summary>
/// <remarks>
/// startup benchmark 只需要验证单 handler publish 的首击路径,
/// 因此这里继续使用单点手工注册,避免把更广泛的注册协调逻辑混入结果。
/// </remarks>
private static MicrosoftDiContainer CreateGFrameworkContainer()
{
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
{
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
});
}
/// <summary>
/// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。
/// </summary>
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
/// <returns>可直接发布 notification 的 runtime。</returns>
private static ICqrsRuntime CreateGFrameworkRuntime(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger);
}
/// <summary>
/// 构建只承载当前 benchmark notification handler 的最小 MediatR 对照宿主。
/// </summary>
/// <returns>可直接解析 <see cref="IPublisher" /> 的 DI 宿主。</returns>
private static ServiceProvider CreateMediatRServiceProvider()
{
return BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(NotificationStartupBenchmarks),
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
ServiceLifetime.Transient);
}
/// <summary>
/// 构建只承载当前 benchmark notification handler 的最小 `Mediator` 对照宿主。
/// </summary>
/// <returns>可直接解析 generated `Mediator.Mediator` 的 DI 宿主。</returns>
private static ServiceProvider CreateMediatorServiceProvider()
{
return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
}
/// <summary>
/// 为 benchmark 创建稳定的 fatal 级 logger避免把日志成本混入 startup 测量。
/// </summary>
/// <param name="categoryName">logger 分类名。</param>
/// <returns>当前 benchmark 使用的稳定 logger。</returns>
private static ILogger CreateLogger(string categoryName)
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
return LoggerFactoryResolver.Provider.CreateLogger(categoryName);
}
/// <summary>
/// Benchmark notification。
/// </summary>
/// <param name="Id">通知标识。</param>
public sealed record BenchmarkNotification(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.INotification,
Mediator.INotification,
MediatR.INotification;
/// <summary>
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 notification handler。
/// </summary>
public sealed class BenchmarkNotificationHandler :
GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>,
Mediator.INotificationHandler<BenchmarkNotification>,
MediatR.INotificationHandler<BenchmarkNotification>
{
/// <summary>
/// 处理 GFramework.CQRS notification。
/// </summary>
/// <param name="notification">当前 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示处理完成的值任务。</returns>
public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(notification);
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.CompletedTask;
}
/// <summary>
/// 处理 NuGet `Mediator` notification。
/// </summary>
/// <param name="notification">当前 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示处理完成的值任务。</returns>
ValueTask Mediator.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return Handle(notification, cancellationToken);
}
/// <summary>
/// 处理 MediatR notification。
/// </summary>
/// <param name="notification">当前 notification。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示处理完成的任务。</returns>
Task MediatR.INotificationHandler<BenchmarkNotification>.Handle(
BenchmarkNotification notification,
CancellationToken cancellationToken)
{
return Handle(notification, cancellationToken).AsTask();
}
}
}

View File

@ -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;
/// <summary>
/// 对比 request 宿主的初始化与首次分发成本,作为后续吸收 `Mediator` comparison benchmark 设计的 startup 基线
/// 对比 request 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次分发成本
/// </summary>
[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!;
/// <summary>
@ -63,6 +66,8 @@ public class RequestStartupBenchmarks
_serviceProvider = CreateMediatRServiceProvider();
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
_mediatorServiceProvider = CreateMediatorServiceProvider();
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
_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);
}
/// <summary>
@ -109,6 +114,16 @@ public class RequestStartupBenchmarks
return _runtime;
}
/// <summary>
/// 返回已构建宿主中的 `Mediator` concrete mediator作为 source-generated 对照组的初始化句柄。
/// </summary>
[Benchmark]
[BenchmarkCategory("Initialization")]
public GeneratedMediator Initialization_Mediator()
{
return _mediator;
}
/// <summary>
/// 在新宿主上首次发送 request作为 MediatR 的 cold-start baseline。
/// </summary>
@ -133,6 +148,18 @@ public class RequestStartupBenchmarks
return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 在新的 `Mediator` 宿主上首次发送 request量化 source-generated concrete path 的 cold-start 成本。
/// </summary>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public async ValueTask<BenchmarkResponse> ColdStart_Mediator()
{
using var serviceProvider = CreateMediatorServiceProvider();
var mediator = serviceProvider.GetRequiredService<GeneratedMediator>();
return await mediator.Send(Request, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
/// </summary>
@ -170,6 +197,14 @@ public class RequestStartupBenchmarks
ServiceLifetime.Transient);
}
/// <summary>
/// 构建只承载当前 benchmark request 的最小 `Mediator` 对照宿主。
/// </summary>
private static ServiceProvider CreateMediatorServiceProvider()
{
return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
}
/// <summary>
/// 为 benchmark 创建稳定的 fatal 级 logger避免把日志成本混入 startup 测量。
/// </summary>
@ -188,6 +223,7 @@ public class RequestStartupBenchmarks
/// <param name="Id">请求标识。</param>
public sealed record BenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
Mediator.IRequest<BenchmarkResponse>,
MediatR.IRequest<BenchmarkResponse>;
/// <summary>
@ -197,10 +233,11 @@ public class RequestStartupBenchmarks
public sealed record BenchmarkResponse(Guid Id);
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。
/// </summary>
public sealed class BenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
{
/// <summary>
@ -211,6 +248,16 @@ public class RequestStartupBenchmarks
return ValueTask.FromResult(new BenchmarkResponse(request.Id));
}
/// <summary>
/// 处理 NuGet `Mediator` request。
/// </summary>
ValueTask<BenchmarkResponse> Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
BenchmarkRequest request,
CancellationToken cancellationToken)
{
return Handle(request, cancellationToken);
}
/// <summary>
/// 处理 MediatR request。
/// </summary>

View File

@ -0,0 +1,430 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
typeof(GFramework.Cqrs.Benchmarks.Messaging.StreamStartupBenchmarks.GeneratedRegistry))]
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 对比 stream 宿主在 GFramework.CQRS reflection / generated 与 MediatR 之间的初始化与首次建流命中成本。
/// </summary>
/// <remarks>
/// 该场景与 <see cref="RequestStartupBenchmarks" /> 保持相同的 `Initialization + ColdStart` 结构,
/// 但 cold-start 边界改为“新宿主 + 首个元素命中”,因为 stream 的首个 <c>MoveNextAsync</c>
/// 才会真正覆盖建流后的首次处理链路。
/// </remarks>
[Config(typeof(Config))]
public class StreamStartupBenchmarks
{
private static readonly ILogger ReflectionRuntimeLogger = CreateLogger(nameof(StreamStartupBenchmarks) + ".Reflection");
private static readonly ILogger GeneratedRuntimeLogger = CreateLogger(nameof(StreamStartupBenchmarks) + ".Generated");
private static readonly BenchmarkStreamRequest Request = new(Guid.NewGuid(), 3);
private MicrosoftDiContainer _reflectionContainer = null!;
private ICqrsRuntime _reflectionRuntime = null!;
private MicrosoftDiContainer _generatedContainer = null!;
private ICqrsRuntime _generatedRuntime = null!;
private ServiceProvider _serviceProvider = null!;
private IMediator _mediatr = null!;
/// <summary>
/// 配置 stream startup benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default
.WithId("ColdStart")
.WithInvocationCount(1)
.WithUnrollFactor(1));
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamStartup"), TargetMethodColumn.Method, CategoriesColumn.Default);
AddDiagnoser(MemoryDiagnoser.Default);
AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByCategory);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建 startup benchmark 复用的 reflection / generated / MediatR 宿主对象。
/// </summary>
[GlobalSetup]
public void Setup()
{
Fixture.Setup("StreamStartup", handlerCount: 1, pipelineCount: 0);
_reflectionContainer = CreateReflectionContainer();
_reflectionRuntime = CreateRuntime(_reflectionContainer, ReflectionRuntimeLogger);
_generatedContainer = CreateGeneratedContainer();
_generatedRuntime = CreateRuntime(_generatedContainer, GeneratedRuntimeLogger);
_serviceProvider = CreateMediatRServiceProvider();
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
}
/// <summary>
/// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保首次绑定路径可重复观察。
/// </summary>
[IterationSetup]
public void ResetColdStartCaches()
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
/// <summary>
/// 释放 startup benchmark 复用的宿主对象。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider);
}
/// <summary>
/// 返回已构建宿主中的 MediatR mediator作为 initialization 组的句柄解析 baseline。
/// </summary>
/// <returns>当前 benchmark 复用的 MediatR mediator。</returns>
[Benchmark(Baseline = true)]
[BenchmarkCategory("Initialization")]
public IMediator Initialization_MediatR()
{
return _mediatr;
}
/// <summary>
/// 返回已构建宿主中的 GFramework.CQRS reflection runtime观察默认 stream binding 宿主句柄解析成本。
/// </summary>
/// <returns>当前 benchmark 复用的 reflection CQRS runtime。</returns>
[Benchmark]
[BenchmarkCategory("Initialization")]
public ICqrsRuntime Initialization_GFrameworkReflection()
{
return _reflectionRuntime;
}
/// <summary>
/// 返回已构建宿主中的 GFramework.CQRS generated runtime观察 generated stream invoker 宿主句柄解析成本。
/// </summary>
/// <returns>当前 benchmark 复用的 generated CQRS runtime。</returns>
[Benchmark]
[BenchmarkCategory("Initialization")]
public ICqrsRuntime Initialization_GFrameworkGenerated()
{
return _generatedRuntime;
}
/// <summary>
/// 在新宿主上首次创建并推进 stream作为 MediatR 的 cold-start baseline。
/// </summary>
/// <returns>首个 stream 响应元素。</returns>
[Benchmark(Baseline = true)]
[BenchmarkCategory("ColdStart")]
public async Task<BenchmarkResponse> ColdStart_MediatR()
{
using var serviceProvider = CreateMediatRServiceProvider();
var mediator = serviceProvider.GetRequiredService<IMediator>();
return await ConsumeFirstItemAsync(mediator.CreateStream(Request, CancellationToken.None), CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 在新的 reflection runtime 上首次创建并推进 stream量化默认 stream binding 的 first-hit 成本。
/// </summary>
/// <returns>首个 stream 响应元素。</returns>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public async ValueTask<BenchmarkResponse> ColdStart_GFrameworkReflection()
{
using var container = CreateReflectionContainer();
var runtime = CreateRuntime(container, ReflectionRuntimeLogger);
return await ConsumeFirstItemAsync(
runtime.CreateStream(BenchmarkContext.Instance, Request, CancellationToken.None),
CancellationToken.None)
.ConfigureAwait(false);
}
/// <summary>
/// 在新的 generated runtime 上首次创建并推进 stream量化 generated stream invoker 路径的 first-hit 成本。
/// </summary>
/// <returns>首个 stream 响应元素。</returns>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public async ValueTask<BenchmarkResponse> ColdStart_GFrameworkGenerated()
{
using var container = CreateGeneratedContainer();
var runtime = CreateRuntime(container, GeneratedRuntimeLogger);
return await ConsumeFirstItemAsync(
runtime.CreateStream(BenchmarkContext.Instance, Request, CancellationToken.None),
CancellationToken.None)
.ConfigureAwait(false);
}
/// <summary>
/// 构建只承载当前 benchmark handler 的最小 reflection GFramework.CQRS 容器。
/// </summary>
private static MicrosoftDiContainer CreateReflectionContainer()
{
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
{
container.RegisterTransient<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
BenchmarkStreamHandler>();
});
}
/// <summary>
/// 构建只承载当前 benchmark generated registry 的最小 generated GFramework.CQRS 容器。
/// </summary>
private static MicrosoftDiContainer CreateGeneratedContainer()
{
return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
{
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRegistry>(container);
});
}
/// <summary>
/// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。
/// </summary>
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
/// <param name="logger">当前 runtime 使用的 benchmark logger。</param>
private static ICqrsRuntime CreateRuntime(MicrosoftDiContainer container, ILogger logger)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(logger);
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, logger);
}
/// <summary>
/// 构建只承载当前 benchmark handler 的最小 MediatR 对照宿主。
/// </summary>
private static ServiceProvider CreateMediatRServiceProvider()
{
return BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(StreamStartupBenchmarks),
static candidateType => candidateType == typeof(BenchmarkStreamHandler),
ServiceLifetime.Transient);
}
/// <summary>
/// 推进 stream 到首个元素,并返回该元素作为 cold-start 结果。
/// </summary>
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
/// <param name="responses">待推进的异步响应序列。</param>
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
/// <returns>首个元素。</returns>
/// <exception cref="InvalidOperationException">stream 未产生任何元素。</exception>
private static async ValueTask<TResponse> ConsumeFirstItemAsync<TResponse>(
IAsyncEnumerable<TResponse> responses,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(responses);
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
await using (enumerator.ConfigureAwait(false))
{
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
{
return enumerator.Current;
}
}
throw new InvalidOperationException("The benchmark stream must yield at least one response.");
}
/// <summary>
/// 为 benchmark 创建稳定的 fatal 级 logger避免把日志成本混入 startup 测量。
/// </summary>
private static ILogger CreateLogger(string categoryName)
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
return LoggerFactoryResolver.Provider.CreateLogger(categoryName);
}
/// <summary>
/// Benchmark stream request。
/// </summary>
/// <param name="Id">请求标识。</param>
/// <param name="ItemCount">返回元素数量。</param>
public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<BenchmarkResponse>,
MediatR.IStreamRequest<BenchmarkResponse>;
/// <summary>
/// Benchmark stream response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record BenchmarkResponse(Guid Id);
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。
/// </summary>
public sealed class BenchmarkStreamHandler :
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>
{
/// <summary>
/// 处理 GFramework.CQRS stream request。
/// </summary>
/// <param name="request">当前 stream 请求。</param>
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
/// <returns>按请求元素数量延迟生成的异步响应序列。</returns>
public IAsyncEnumerable<BenchmarkResponse> Handle(
BenchmarkStreamRequest request,
CancellationToken cancellationToken)
{
return EnumerateAsync(request, cancellationToken);
}
/// <summary>
/// 处理 MediatR stream request。
/// </summary>
/// <param name="request">当前 stream 请求。</param>
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
/// <returns>按请求元素数量延迟生成的异步响应序列。</returns>
IAsyncEnumerable<BenchmarkResponse> MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
BenchmarkStreamRequest request,
CancellationToken cancellationToken)
{
return EnumerateAsync(request, cancellationToken);
}
/// <summary>
/// 生成固定长度的 benchmark stream确保 cold-start 与 steady-state 维度共用同一份响应形状。
/// </summary>
/// <param name="request">当前 stream 请求。</param>
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
/// <returns>按请求数量生成的异步响应序列。</returns>
private static async IAsyncEnumerable<BenchmarkResponse> EnumerateAsync(
BenchmarkStreamRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
for (var index = 0; index < request.ItemCount; index++)
{
cancellationToken.ThrowIfCancellationRequested();
yield return new BenchmarkResponse(request.Id);
await Task.CompletedTask.ConfigureAwait(false);
}
}
}
/// <summary>
/// 为 stream startup benchmark 提供 hand-written generated registry
/// 以便独立比较 generated stream invoker 的初始化与首次命中成本。
/// </summary>
public sealed class GeneratedRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsStreamInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
new(
typeof(GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
BenchmarkStreamRequest,
BenchmarkResponse>),
typeof(GeneratedRegistry).GetMethod(
nameof(InvokeBenchmarkStreamHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated stream startup benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
typeof(BenchmarkStreamRequest),
typeof(BenchmarkResponse),
Descriptor)
];
/// <summary>
/// 把 startup benchmark handler 注册为 transient保持与 cold-start 对照宿主一致的 handler 生命周期。
/// </summary>
/// <param name="services">承载 generated handler 注册结果的目标服务集合。</param>
/// <param name="logger">记录 generated registry 注册过程的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
BenchmarkStreamRequest,
BenchmarkResponse>),
typeof(BenchmarkStreamHandler));
logger.Debug("Registered generated stream startup benchmark handler.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
/// </summary>
/// <returns>当前 startup benchmark 的 generated stream invoker 描述符集合。</returns>
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
/// </summary>
/// <param name="requestType">待匹配的 stream 请求类型。</param>
/// <param name="responseType">待匹配的 stream 响应类型。</param>
/// <param name="descriptor">匹配成功时返回的 generated stream invoker 描述符。</param>
/// <returns>命中当前 benchmark 请求/响应类型对时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
{
if (requestType == typeof(BenchmarkStreamRequest) &&
responseType == typeof(BenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated stream invoker provider 为 startup benchmark 产出的开放静态调用入口。
/// </summary>
/// <param name="handler">当前 benchmark 注册的 stream handler 实例。</param>
/// <param name="request">当前 benchmark 的 stream 请求对象。</param>
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
/// <returns>由 handler 产生的异步响应序列。</returns>
public static object InvokeBenchmarkStreamHandler(object handler, object request, CancellationToken cancellationToken)
{
var typedHandler = (GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<
BenchmarkStreamRequest,
BenchmarkResponse>)handler;
var typedRequest = (BenchmarkStreamRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}
}

View File

@ -1,39 +1,53 @@
# GFramework.Cqrs.Benchmarks
该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于持续比较运行时 dispatch、publish、cold-start 与后续 generator / pipeline 收口的成本变化
该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于在当前 HEAD 上复核 request、stream、notification 的 steady-state 与 startup 成本边界
## 目的
- 为 `GFramework.Cqrs` 建立独立于 NUnit 集成测试的 BenchmarkDotNet 基线
- 参考 `ai-libs/Mediator/benchmarks` 的场景组织方式,逐步补齐 request、notification、stream 与初始化成本对比
- 为后续吸收 `Mediator` 的 dispatch 设计、fixture 组织和对比矩阵提供可重复验证入口
- 为 `GFramework.Cqrs` 提供独立于测试工程的 BenchmarkDotNet 复核入口
- 让 request、stream、notification 的热路径与 cold-start 变化有可重复的对照矩阵
- 在不引入“未来已存在”假设的前提下,明确当前 benchmark 已覆盖什么、还没有覆盖什么
## 当前内容
## 当前 coverage
- `Program.cs`
- benchmark 命令行入口
- `Messaging/Fixture.cs`
- 运行前输出并校验场景配置
- `Messaging/RequestBenchmarks.cs`
- direct handler、NuGet `Mediator` source-generated concrete path、已接上 handwritten generated request invoker provider 的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestLifetimeBenchmarks.cs`
- `Singleton / Scoped / Transient` 三类 handler 生命周期下direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比;其中 `Scoped` 通过真实 request 级作用域宿主执行,不再把 scoped handler 退化为根容器解析
- `Messaging/StreamLifetimeBenchmarks.cs`
- `Singleton / Scoped / Transient` 三类 handler 生命周期下direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 分层对照,并同时提供 `FirstItem / DrainAll` 两种观测口径
- `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestStartupBenchmarks.cs`
- `Initialization``ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度
- `Messaging/RequestInvokerBenchmarks.cs`
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/StreamInvokerBenchmarks.cs`
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
- `Messaging/NotificationBenchmarks.cs`
- `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比
- `Messaging/NotificationFanOutBenchmarks.cs`
- fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比
- `Messaging/StreamingBenchmarks.cs`
- direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
当前工程已经覆盖以下矩阵:
- request steady-state
- `Messaging/RequestBenchmarks.cs`
- direct handler、默认 `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR`
- `Messaging/RequestLifetimeBenchmarks.cs`
- `Singleton / Scoped / Transient` 三类 handler 生命周期下baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
- `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
- `Messaging/RequestInvokerBenchmarks.cs`
- baseline、`GFramework.Cqrs` reflection request binding、`GFramework.Cqrs` generated request invoker、`MediatR`
- request startup
- `Messaging/RequestStartupBenchmarks.cs`
- `Initialization``ColdStart` 两组下,`GFramework.Cqrs`、NuGet `Mediator``MediatR`
- stream steady-state
- `Messaging/StreamingBenchmarks.cs`
- baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
- 同时提供 `FirstItem``DrainAll` 两种观测口径
- `Messaging/StreamLifetimeBenchmarks.cs`
- `Singleton / Scoped / Transient` 三类 handler 生命周期下baseline、`GFramework.Cqrs` reflection stream binding、`GFramework.Cqrs` generated stream registry、`MediatR`
- 同时提供 `FirstItem``DrainAll` 两种观测口径
- `Messaging/StreamInvokerBenchmarks.cs`
- baseline、`GFramework.Cqrs` reflection stream binding、`GFramework.Cqrs` generated stream invoker、`MediatR`
- 同时提供 `FirstItem``DrainAll` 两种观测口径
- stream startup
- `Messaging/StreamStartupBenchmarks.cs`
- `Initialization``ColdStart` 两组下,`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR`
- 其中 `ColdStart` 的边界是“新宿主 + 首个元素命中”,不是完整枚举整个 stream
- notification steady-state
- `Messaging/NotificationBenchmarks.cs`
- 单处理器 publish 下,`GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR`
- `Messaging/NotificationLifetimeBenchmarks.cs`
- 单处理器 publish 在 `Singleton / Scoped / Transient` 三类 handler 生命周期下的 baseline、`GFramework.Cqrs``MediatR` 对照
- `Messaging/NotificationFanOutBenchmarks.cs`
- 固定 `4 handler` fan-out 下的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator``MediatR`
- notification startup
- `Messaging/NotificationStartupBenchmarks.cs`
- `Initialization``ColdStart` 两组下,`GFramework.Cqrs`、NuGet `Mediator``MediatR`
## 最小使用方式
@ -56,33 +70,33 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks.Stream_*"
```
如果需要在两个终端里并发复核不同的过滤 benchmark请为每个进程追加不同的 `--artifacts-suffix <suffix>`,把 `BenchmarkDotNet` auto-generated build 与 artifacts 输出隔离到不同目录;这只是运行入口的目录隔离约定,不是 benchmark 业务逻辑本身的要求。例如:
## 并发运行约束
当两个 benchmark 进程需要并发运行时,必须为每个进程追加不同的 `--artifacts-suffix <suffix>`。当前入口会把这个 suffix 解析成独立的 `BenchmarkDotNet.Artifacts/<suffix>/` 目录,并在该目录下复制隔离的 benchmark host避免多个进程写入同一份 auto-generated build 与 artifacts 输出。
例如:
```bash
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix req-lifetime-a --filter "*RequestLifetimeBenchmarks.SendRequest_*"
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix stream-lifetime-b --filter "*StreamLifetimeBenchmarks.Stream_*"
```
## 当前约束
如果不并发运行,就不需要额外传入 `--artifacts-suffix``BenchmarkDotNet.Artifacts/` 仍然是本地生成输出,默认不作为常规提交内容。
- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容
- 当两个带 `--filter` 的 benchmark 进程需要并发运行时,必须为它们分别传入不同的 `--artifacts-suffix <suffix>`,避免多个 `BenchmarkDotNet` 进程写入同一份 auto-generated build / artifacts 目录;这个约束只服务于本地输出隔离,不代表 benchmark 场景之间存在额外业务依赖
- `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径
- `RequestLifetimeBenchmarks``Scoped` 场景会复用单个 scoped runtime但在每次 request 分发时仍显式创建并释放真实 DI 作用域,用来观察 scoped handler 绑定到 request 边界后的解析与 dispatch 成本,而不是把 runtime 构造常量成本混进生命周期对照
- `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem``DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察
- `StreamingBenchmarks``StreamInvokerBenchmarks` 都同时暴露 `FirstItem``DrainAll`;阅读结果时应把它们分别理解为“建流到首个元素”的固定成本观测与“完整枚举整个 stream”的总成本观测
- `StreamInvokerBenchmarks` 当前的 `DrainAll` short-job 输出只适合做 smoke 复核,确认矩阵和路径可以正常跑通;它不应直接写成 reflection、generated 或 `MediatR` 之间的稳定性能结论,若要做排序判断,应复跑默认作业或更完整的 benchmark 批次
- 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主就应至少复跑能覆盖该路径的过滤场景request 热路径通常先看:
- `RequestBenchmarks.SendRequest_*`
- `RequestLifetimeBenchmarks.SendRequest_*`
- 只要变更影响 stream dispatch、建流绑定或相关宿主接线就应补跑
- `StreamingBenchmarks.Stream_*`
- `StreamLifetimeBenchmarks.Stream_*`
- 当前性能目标不是超过 source-generated `Mediator`,而是让默认 request steady-state 路径尽量接近它,并至少稳定快于基于反射 / 扫描的 `MediatR`
## 结果解读边界
## 后续扩展方向
- `RequestLifetimeBenchmarks``Scoped` 场景会在每次 request 分发时显式创建并释放真实 DI 作用域;它观察的是 scoped handler 的解析与 dispatch 成本,不把 runtime 构造常量成本混入生命周期对照
- `NotificationLifetimeBenchmarks``Scoped` 场景也采用真实 DI 作用域;它比较的是 publish 路径上的生命周期额外开销,不是根容器解析退化后的近似值
- `StreamingBenchmarks``StreamLifetimeBenchmarks``StreamInvokerBenchmarks` 同时暴露 `FirstItem``DrainAll`
- `FirstItem` 适合观察“建流到首个元素”的固定成本
- `DrainAll` 适合观察完整枚举整个 stream 的总成本
- `StreamStartupBenchmarks``ColdStart` 只推进到首个元素,因此它回答的是“新宿主下首次建流命中”的边界,不回答完整枚举总成本
- 当前 HEAD 没有单独固化的 short-job benchmark 类或 checked-in short-job 结果;如果手动使用 short job / short run 只做 smoke 复核,应把它理解为“确认矩阵与路径能跑通”
- 特别是 `StreamInvokerBenchmarks``DrainAll` 在 short-job smoke 下不应直接写成 reflection、generated 或 `MediatR` 之间的稳定排序结论;若要比较名次或小幅差值,应复跑默认作业或更完整的批次
- 若继续优化 stream lifetime可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景
## 当前缺口
- 当前没有 stream 版的 NuGet `Mediator` source-generated concrete path 对照stream steady-state、lifetime、startup 现在都只覆盖 `GFramework.Cqrs``MediatR`
- 当前没有 request 生命周期下的 NuGet `Mediator` compile-time lifetime 矩阵;`RequestLifetimeBenchmarks` 只覆盖 `GFramework.Cqrs``MediatR`
- 当前没有 notification fan-out 的生命周期矩阵;`NotificationFanOutBenchmarks` 只覆盖固定 `4 handler` 的已装配宿主
- 当前没有 stream pipeline benchmark现有 pipeline coverage 仅限 request

View File

@ -309,6 +309,60 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 request dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
/// </summary>
[Test]
public async Task Dispatcher_Should_Create_Request_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
{
var requestBindings = GetCacheField("RequestDispatchBindings");
var behaviorType = typeof(IPipelineBehavior<DispatcherPipelineCacheRequest, int>);
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);
});
}
/// <summary>
/// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。
/// </summary>
@ -374,6 +428,60 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 stream dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
/// </summary>
[Test]
public async Task Dispatcher_Should_Create_Stream_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
{
var streamBindings = GetCacheField("StreamDispatchBindings");
var behaviorType = typeof(IStreamPipelineBehavior<DispatcherCacheStreamRequest, int>);
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);
});
}
/// <summary>
/// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。
/// </summary>
@ -492,6 +600,71 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 request dispatch binding 先在零行为 dispatcher 上命中后,
/// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler并为当前上下文重新注入架构实例。
/// </summary>
[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));
});
}
/// <summary>
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler
/// 并为当次实例重新注入当前架构上下文。
@ -632,6 +805,108 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 stream dispatch binding 先在零行为 dispatcher 上命中后,
/// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler并为当前上下文重新注入架构实例。
/// </summary>
[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));
});
}
/// <summary>
/// 描述缓存测试 fixture 需要启用的可选 pipeline 行为集合,
/// 用于构造“同一静态 binding 对应不同 dispatcher 注册可见性”的组合场景。
/// </summary>
private sealed class DispatcherCacheFixtureOptions
{
/// <summary>
/// 获取是否注册 <see cref="DispatcherPipelineCacheBehavior" />。
/// </summary>
public bool IncludeRequestPipelineCacheBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 <see cref="DispatcherPipelineContextRefreshBehavior" />。
/// </summary>
public bool IncludeRequestPipelineContextRefreshBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 request 顺序验证所需的两层 pipeline 行为。
/// </summary>
public bool IncludeRequestPipelineOrderBehaviors { get; init; } = true;
/// <summary>
/// 获取是否注册 <see cref="DispatcherStreamPipelineCacheBehavior" />。
/// </summary>
public bool IncludeStreamPipelineCacheBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 <see cref="DispatcherStreamPipelineContextRefreshBehavior" />。
/// </summary>
public bool IncludeStreamPipelineContextRefreshBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 stream 顺序验证所需的两层 pipeline 行为。
/// </summary>
public bool IncludeStreamPipelineOrderBehaviors { get; init; } = true;
}
/// <summary>
/// 通过反射读取 dispatcher 的静态缓存对象。
/// </summary>
@ -679,10 +954,11 @@ internal sealed class CqrsDispatcherCacheTests
/// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器,
/// 用于验证 dispatcher 的实例级缓存不会跨容器共享。
/// </summary>
private static MicrosoftDiContainer CreateFrozenContainer()
/// <param name="options">控制当前隔离容器要启用哪些可选 pipeline 行为的配置。</param>
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 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。
/// </summary>
/// <param name="container">待补齐 CQRS 注册的目标容器。</param>
private static void ConfigureDispatcherCacheFixture(MicrosoftDiContainer container)
/// <param name="options">控制是否跳过特定 pipeline 行为注册的可选配置。</param>
private static void ConfigureDispatcherCacheFixture(
MicrosoftDiContainer container,
DispatcherCacheFixtureOptions? options = null)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>();
options ??= new DispatcherCacheFixtureOptions();
if (options.IncludeRequestPipelineCacheBehavior)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
}
if (options.IncludeRequestPipelineContextRefreshBehavior)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
}
if (options.IncludeRequestPipelineOrderBehaviors)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
}
if (options.IncludeStreamPipelineCacheBehavior)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>();
}
if (options.IncludeStreamPipelineContextRefreshBehavior)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>();
}
if (options.IncludeStreamPipelineOrderBehaviors)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>();
}
CqrsTestRuntime.RegisterHandlers(
container,

View File

@ -448,6 +448,166 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 验证当 generated request invoker provider 的 descriptor 枚举抛出异常时,
/// registrar 会跳过 generated descriptor 预热并回退到反射路径。
/// </summary>
[Test]
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Descriptor_Enumeration_Throws()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(ThrowingEnumeratingRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.ThrowingEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("runtime:payload"));
}
/// <summary>
/// 验证当 generated stream invoker provider 的 descriptor 枚举抛出异常时,
/// registrar 会跳过 generated descriptor 预热并回退到反射建流路径。
/// </summary>
[Test]
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Descriptor_Enumeration_Throws()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(ThrowingEnumeratingStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.ThrowingEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 验证当 request descriptor 枚举返回重复 request-response pair 时,
/// registrar 会稳定保留首个有效描述符,并忽略后续重复项。
/// </summary>
[Test]
public async Task SendAsync_Should_Use_First_Generated_Request_Descriptor_When_Duplicates_Are_Enumerated()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(DuplicateEnumeratingRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.DuplicateEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("generated:payload"));
}
/// <summary>
/// 验证当 stream descriptor 枚举返回重复 request-response pair 时,
/// registrar 会稳定保留首个有效描述符,并忽略后续重复项。
/// </summary>
[Test]
public async Task CreateStream_Should_Use_First_Generated_Stream_Descriptor_When_Duplicates_Are_Enumerated()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(DuplicateEnumeratingStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.DuplicateEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([30, 31]));
}
/// <summary>
/// 验证当 request descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
/// registrar 会忽略该坏 descriptor并继续回退到反射路径。
/// </summary>
[Test]
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Enumerated_Request_Descriptor_Does_Not_Match_Provider()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(MismatchedEnumeratingRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.MismatchedEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("runtime:payload"));
}
/// <summary>
/// 验证当首个 request descriptor 无效、后续同键 descriptor 有效时,
/// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。
/// </summary>
[Test]
public async Task SendAsync_Should_Use_Later_Valid_Generated_Request_Descriptor_When_First_Duplicate_Is_Invalid()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.InvalidThenValidDuplicateRequestInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
Assert.That(response, Is.EqualTo("generated:payload"));
}
/// <summary>
/// 验证当 stream descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
/// registrar 会忽略该坏 descriptor并继续回退到反射建流路径。
/// </summary>
[Test]
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Enumerated_Stream_Descriptor_Does_Not_Match_Provider()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(MismatchedEnumeratingStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.MismatchedEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([3, 4]));
}
/// <summary>
/// 验证当首个 stream descriptor 无效、后续同键 descriptor 有效时,
/// registrar 不会因为过早去重而丢掉本可注册的 generated descriptor。
/// </summary>
[Test]
public async Task CreateStream_Should_Use_Later_Valid_Generated_Stream_Descriptor_When_First_Duplicate_Is_Invalid()
{
var generatedAssembly = CreateGeneratedAssembly(
typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry),
"GFramework.Cqrs.Tests.Cqrs.InvalidThenValidDuplicateStreamInvokerAssembly, Version=1.0.0.0");
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var context = new ArchitectureContext(container);
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
Assert.That(results, Is.EqualTo([30, 31]));
}
/// <summary>
/// 模拟返回实例 request invoker 方法的 generated registry。
/// </summary>
@ -860,6 +1020,529 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
}
}
/// <summary>
/// 模拟 descriptor 枚举阶段抛出异常的 request invoker provider。
/// </summary>
private sealed class ThrowingEnumeratingRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptor Descriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
throw new InvalidOperationException("request descriptors failed");
}
}
/// <summary>
/// 模拟 descriptor 枚举阶段抛出异常的 stream invoker provider。
/// </summary>
private sealed class ThrowingEnumeratingStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
throw new InvalidOperationException("stream descriptors failed");
}
}
/// <summary>
/// 模拟返回重复 request descriptor 条目的 generated registry。
/// </summary>
private sealed class DuplicateEnumeratingRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptor PrimaryDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsRequestInvokerDescriptor SecondaryDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(DuplicateEnumeratingRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = PrimaryDescriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsRequestInvokerDescriptorEntry(typeof(GeneratedRequestInvokerRequest), typeof(string), PrimaryDescriptor),
new CqrsRequestInvokerDescriptorEntry(typeof(GeneratedRequestInvokerRequest), typeof(string), SecondaryDescriptor)
];
}
private static ValueTask<string> InvokeAlternativeGenerated(
object handler,
object request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult("duplicate:payload");
}
}
/// <summary>
/// 模拟返回重复 stream descriptor 条目的 generated registry。
/// </summary>
private sealed class DuplicateEnumeratingStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptor PrimaryDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsStreamInvokerDescriptor SecondaryDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(DuplicateEnumeratingStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = PrimaryDescriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsStreamInvokerDescriptorEntry(typeof(GeneratedStreamInvokerRequest), typeof(int), PrimaryDescriptor),
new CqrsStreamInvokerDescriptorEntry(typeof(GeneratedStreamInvokerRequest), typeof(int), SecondaryDescriptor)
];
}
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
{
return new[] { 900, 901 }.ToAsyncEnumerable();
}
}
/// <summary>
/// 模拟枚举出的 request descriptor 与 provider 显式查询结果不一致的 generated registry。
/// </summary>
private sealed class MismatchedEnumeratingRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptor ProviderDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsRequestInvokerDescriptor EnumeratedDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(MismatchedEnumeratingRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = ProviderDescriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsRequestInvokerDescriptorEntry(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
EnumeratedDescriptor)
];
}
private static ValueTask<string> InvokeAlternativeGenerated(
object handler,
object request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult("mismatched:payload");
}
}
/// <summary>
/// 模拟首个 request descriptor 无效、后续同键 descriptor 有效的 generated registry。
/// </summary>
private sealed class InvalidThenValidDuplicateRequestInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsRequestInvokerProvider,
IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly CqrsRequestInvokerDescriptor InvalidDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(InvalidThenValidDuplicateRequestInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsRequestInvokerDescriptor ValidDescriptor = new(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
typeof(GeneratedRequestInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsRequestInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
{
descriptor = ValidDescriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsRequestInvokerDescriptorEntry(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
InvalidDescriptor),
new CqrsRequestInvokerDescriptorEntry(
typeof(GeneratedRequestInvokerRequest),
typeof(string),
ValidDescriptor)
];
}
private static ValueTask<string> InvokeAlternativeGenerated(
object handler,
object request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult("invalid-first:payload");
}
}
/// <summary>
/// 模拟枚举出的 stream descriptor 与 provider 显式查询结果不一致的 generated registry。
/// </summary>
private sealed class MismatchedEnumeratingStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptor ProviderDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsStreamInvokerDescriptor EnumeratedDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(MismatchedEnumeratingStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = ProviderDescriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsStreamInvokerDescriptorEntry(
typeof(GeneratedStreamInvokerRequest),
typeof(int),
EnumeratedDescriptor)
];
}
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
{
return new[] { 700, 701 }.ToAsyncEnumerable();
}
}
/// <summary>
/// 模拟首个 stream descriptor 无效、后续同键 descriptor 有效的 generated registry。
/// </summary>
private sealed class InvalidThenValidDuplicateStreamInvokerProviderRegistry :
ICqrsHandlerRegistry,
ICqrsStreamInvokerProvider,
IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly CqrsStreamInvokerDescriptor InvalidDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(InvalidThenValidDuplicateStreamInvokerProviderRegistry).GetMethod(
nameof(InvokeAlternativeGenerated),
BindingFlags.NonPublic | BindingFlags.Static)!);
private static readonly CqrsStreamInvokerDescriptor ValidDescriptor = new(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
"InvokeGenerated",
BindingFlags.NonPublic | BindingFlags.Static)!);
/// <inheritdoc />
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
typeof(GeneratedStreamInvokerRequestHandler));
}
/// <inheritdoc />
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out CqrsStreamInvokerDescriptor? descriptor)
{
ArgumentNullException.ThrowIfNull(requestType);
ArgumentNullException.ThrowIfNull(responseType);
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
{
descriptor = ValidDescriptor;
return true;
}
descriptor = null;
return false;
}
/// <inheritdoc />
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return
[
new CqrsStreamInvokerDescriptorEntry(
typeof(GeneratedStreamInvokerRequest),
typeof(int),
InvalidDescriptor),
new CqrsStreamInvokerDescriptorEntry(
typeof(GeneratedStreamInvokerRequest),
typeof(int),
ValidDescriptor)
];
}
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
{
return new[] { 800, 801 }.ToAsyncEnumerable();
}
}
/// <summary>
/// 创建带有 generated request invoker registry 元数据的程序集替身。
/// </summary>

View File

@ -91,6 +91,70 @@ internal sealed class CqrsNotificationPublisherTests
Assert.That(handler.ObservedContext, Is.SameAs(architectureContext.Object));
}
/// <summary>
/// 验证当容器里可见多个通知发布策略时dispatcher 会拒绝在歧义状态下继续发布。
/// </summary>
[Test]
public void PublishAsync_Should_Throw_When_Multiple_NotificationPublishers_Are_Registered()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<PublisherNotification>)))
.Returns([new RecordingNotificationHandler("only", [])]);
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
.Returns(
[
new TrackingNotificationPublisher(),
new TrackingNotificationPublisher()
]);
});
Assert.That(
async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.EqualTo(
$"Multiple {typeof(INotificationPublisher).FullName} instances are registered. Remove duplicate notification publisher strategies before publishing notifications."));
}
/// <summary>
/// 验证 dispatcher 在首次发布时解析通知发布器后,会复用同一实例并停止继续查询容器。
/// </summary>
[Test]
public async Task PublishAsync_Should_Cache_Resolved_NotificationPublisher_After_First_Publish()
{
var firstPublisher = new TrackingNotificationPublisher();
var secondPublisher = new TrackingNotificationPublisher();
var notificationPublisherLookupCount = 0;
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<PublisherNotification>)))
.Returns([new RecordingNotificationHandler("only", [])]);
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
.Returns(() =>
{
notificationPublisherLookupCount++;
return notificationPublisherLookupCount switch
{
1 => [firstPublisher],
2 => [secondPublisher],
_ => throw new AssertionException("Notification publisher should be resolved at most once.")
};
});
});
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
Assert.That(notificationPublisherLookupCount, Is.EqualTo(1));
Assert.That(firstPublisher.PublishCallCount, Is.EqualTo(2));
Assert.That(secondPublisher.PublishCallCount, Is.Zero);
}
/// <summary>
/// 验证内置 `TaskWhenAll` 发布器会继续调度所有处理器,而不是沿用默认顺序发布器的失败即停语义。
/// </summary>
@ -262,6 +326,11 @@ internal sealed class CqrsNotificationPublisherTests
/// </summary>
public bool WasCalled { get; private set; }
/// <summary>
/// 获取当前发布器累计执行发布的次数。
/// </summary>
public int PublishCallCount { get; private set; }
/// <summary>
/// 记录当前发布器已被调用,并继续按当前顺序执行所有处理器。
/// </summary>
@ -275,6 +344,7 @@ internal sealed class CqrsNotificationPublisherTests
where TNotification : INotification
{
WasCalled = true;
PublishCallCount++;
foreach (var handler in context.Handlers)
{

View File

@ -87,17 +87,125 @@ internal sealed class CqrsRegistrationServiceTests
});
}
/// <summary>
/// 验证当 <see cref="Assembly.FullName" /> 缺失时,协调器会退化到 <see cref="AssemblyName.Name" /> 作为稳定程序集键。
/// </summary>
[Test]
public void RegisterHandlers_Should_Fallback_To_Simple_Name_When_Full_Name_Is_Missing()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
var firstAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
assemblyDisplayName: "DisplayName-A");
var secondAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
assemblyDisplayName: "DisplayName-B");
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([firstAssembly.Object]);
service.RegisterHandlers([secondAssembly.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
var debugMessages = logger.Logs
.Where(static log => log.Level == LogLevel.Debug)
.Select(static log => log.Message)
.ToArray();
Assert.That(debugMessages, Has.Length.EqualTo(1));
Assert.That(debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.SimpleNameFallback"));
Assert.That(debugMessages[0], Does.Not.Contain("DisplayName-B"));
});
}
/// <summary>
/// 验证当 <see cref="Assembly.FullName" /> 与 <see cref="AssemblyName.Name" /> 均缺失时,
/// 协调器会退化到 <see cref="object.ToString" /> 结果作为稳定程序集键。
/// </summary>
[Test]
public void RegisterHandlers_Should_Fallback_To_ToString_When_Full_Name_And_Simple_Name_Are_Missing()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
const string assemblyDisplayName = "GFramework.Cqrs.Tests.ToStringFallback";
var firstAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: null,
assemblyDisplayName: assemblyDisplayName);
var secondAssembly = CreateAssembly(
assemblyFullName: null,
assemblySimpleName: null,
assemblyDisplayName: assemblyDisplayName);
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([firstAssembly.Object]);
service.RegisterHandlers([secondAssembly.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
var debugMessages = logger.Logs
.Where(static log => log.Level == LogLevel.Debug)
.Select(static log => log.Message)
.ToArray();
Assert.That(debugMessages, Has.Length.EqualTo(1));
Assert.That(debugMessages[0], Does.Contain(assemblyDisplayName));
Assert.That(debugMessages[0], Does.Contain("already registered"));
});
}
/// <summary>
/// 创建一个带稳定程序集键的程序集 mock用于模拟不同 <see cref="Assembly" /> 实例表示同一程序集的场景。
/// </summary>
/// <param name="assemblyFullName">要返回的程序集完整名称。</param>
/// <returns>配置好完整名称的程序集 mock。</returns>
private static Mock<Assembly> CreateAssembly(string assemblyFullName)
{
return CreateAssembly(assemblyFullName, assemblySimpleName: null, assemblyDisplayName: assemblyFullName);
}
/// <summary>
/// 创建一个可配置程序集元数据退化路径的程序集 mock用于验证稳定程序集键的回退顺序。
/// </summary>
/// <param name="assemblyFullName">要返回的程序集完整名称;为 <see langword="null" /> 时模拟缺失完整名称。</param>
/// <param name="assemblySimpleName">要返回的程序集简单名称;为 <see langword="null" /> 时模拟缺失简单名称。</param>
/// <param name="assemblyDisplayName">当需要退化到 <see cref="object.ToString" /> 时返回的显示名称。</param>
/// <returns>配置好程序集元数据的程序集 mock。</returns>
private static Mock<Assembly> CreateAssembly(string? assemblyFullName, string? assemblySimpleName, string assemblyDisplayName)
{
var assembly = new Mock<Assembly>();
var assemblyName = new AssemblyName();
assembly
.SetupGet(static currentAssembly => currentAssembly.FullName)
.Returns(assemblyFullName);
assemblyName.Name = assemblySimpleName;
assembly
.Setup(static currentAssembly => currentAssembly.GetName())
.Returns(assemblyName);
assembly
.Setup(static currentAssembly => currentAssembly.ToString())
.Returns(assemblyDisplayName);
return assembly;
}

View File

@ -16,6 +16,13 @@ namespace GFramework.Cqrs.Internal;
/// </summary>
internal static class CqrsHandlerRegistrar
{
/// <summary>
/// 描述 generated invoker descriptor 在 registrar 预热阶段使用的 request/response 类型对键。
/// </summary>
/// <param name="RequestType">请求运行时类型。</param>
/// <param name="ResponseType">响应运行时类型。</param>
private readonly record struct InvokerDescriptorKey(Type RequestType, Type ResponseType);
// 卸载安全的进程级缓存:程序集元数据只按弱键复用。
// 若程序集来自 collectible AssemblyLoadContext被回收后会重新分析而不会被静态缓存永久钉住。
private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
@ -321,8 +328,49 @@ internal static class CqrsHandlerRegistrar
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
return;
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
IReadOnlyList<CqrsRequestInvokerDescriptorEntry>? descriptors;
try
{
descriptors = descriptorSource.GetDescriptors();
}
catch (Exception exception)
{
logger.Warn(
$"Failed to enumerate generated CQRS request invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName}. Falling back to runtime reflection for request invokers: {exception.Message}");
return;
}
if (descriptors is null)
{
logger.Warn(
$"Ignoring generated CQRS request invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName} because GetDescriptors() returned null.");
return;
}
var registeredKeys = new HashSet<InvokerDescriptorKey>();
foreach (var descriptorEntry in descriptors)
{
if (descriptorEntry is null)
{
logger.Warn(
$"Ignoring null generated CQRS request invoker descriptor entry from provider {provider.GetType().FullName} in assembly {assemblyName}.");
continue;
}
var descriptorKey = new InvokerDescriptorKey(
descriptorEntry.RequestType,
descriptorEntry.ResponseType);
if (!TryValidateEnumeratedRequestInvokerDescriptor(provider, descriptorEntry, assemblyName, logger))
continue;
if (!registeredKeys.Add(descriptorKey))
{
logger.Warn(
$"Ignoring duplicate generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName}.");
continue;
}
CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
descriptorEntry.RequestType,
descriptorEntry.ResponseType,
@ -376,8 +424,49 @@ internal static class CqrsHandlerRegistrar
if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource)
return;
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
IReadOnlyList<CqrsStreamInvokerDescriptorEntry>? descriptors;
try
{
descriptors = descriptorSource.GetDescriptors();
}
catch (Exception exception)
{
logger.Warn(
$"Failed to enumerate generated CQRS stream invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName}. Falling back to runtime reflection for stream invokers: {exception.Message}");
return;
}
if (descriptors is null)
{
logger.Warn(
$"Ignoring generated CQRS stream invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName} because GetDescriptors() returned null.");
return;
}
var registeredKeys = new HashSet<InvokerDescriptorKey>();
foreach (var descriptorEntry in descriptors)
{
if (descriptorEntry is null)
{
logger.Warn(
$"Ignoring null generated CQRS stream invoker descriptor entry from provider {provider.GetType().FullName} in assembly {assemblyName}.");
continue;
}
var descriptorKey = new InvokerDescriptorKey(
descriptorEntry.RequestType,
descriptorEntry.ResponseType);
if (!TryValidateEnumeratedStreamInvokerDescriptor(provider, descriptorEntry, assemblyName, logger))
continue;
if (!registeredKeys.Add(descriptorKey))
{
logger.Warn(
$"Ignoring duplicate generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName}.");
continue;
}
CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor(
descriptorEntry.RequestType,
descriptorEntry.ResponseType,
@ -387,6 +476,96 @@ internal static class CqrsHandlerRegistrar
}
}
/// <summary>
/// 校验 request descriptor 枚举项是否与 provider 的显式查询结果保持一致。
/// </summary>
/// <param name="provider">当前正在预热的 request invoker provider。</param>
/// <param name="descriptorEntry">当前枚举到的描述符条目。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>当该枚举项可安全写入 dispatcher 缓存时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryValidateEnumeratedRequestInvokerDescriptor(
ICqrsRequestInvokerProvider provider,
CqrsRequestInvokerDescriptorEntry descriptorEntry,
string assemblyName,
ILogger logger)
{
try
{
if (!provider.TryGetDescriptor(
descriptorEntry.RequestType,
descriptorEntry.ResponseType,
out var resolvedDescriptor) ||
resolvedDescriptor is null)
{
logger.Warn(
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor did not return a matching descriptor.");
return false;
}
if (!resolvedDescriptor.InvokerMethod.Equals(descriptorEntry.Descriptor.InvokerMethod) ||
resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType)
{
logger.Warn(
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because the enumerated descriptor does not match TryGetDescriptor.");
return false;
}
return true;
}
catch (Exception exception)
{
logger.Warn(
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor threw: {exception.Message}");
return false;
}
}
/// <summary>
/// 校验 stream descriptor 枚举项是否与 provider 的显式查询结果保持一致。
/// </summary>
/// <param name="provider">当前正在预热的 stream invoker provider。</param>
/// <param name="descriptorEntry">当前枚举到的描述符条目。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>当该枚举项可安全写入 dispatcher 缓存时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryValidateEnumeratedStreamInvokerDescriptor(
ICqrsStreamInvokerProvider provider,
CqrsStreamInvokerDescriptorEntry descriptorEntry,
string assemblyName,
ILogger logger)
{
try
{
if (!provider.TryGetDescriptor(
descriptorEntry.RequestType,
descriptorEntry.ResponseType,
out var resolvedDescriptor) ||
resolvedDescriptor is null)
{
logger.Warn(
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor did not return a matching descriptor.");
return false;
}
if (!resolvedDescriptor.InvokerMethod.Equals(descriptorEntry.Descriptor.InvokerMethod) ||
resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType)
{
logger.Warn(
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because the enumerated descriptor does not match TryGetDescriptor.");
return false;
}
return true;
}
catch (Exception exception)
{
logger.Warn(
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor threw: {exception.Message}");
return false;
}
}
/// <summary>
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
/// </summary>

View File

@ -72,7 +72,7 @@ dotnet add package GeWuYou.GFramework.Cqrs
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
```
如果你希望减少处理器注册时的反射扫描,再额外安装:
如果你希望把可静态表达的 handler 注册与 request / stream invoker 元数据前移到编译期,再额外安装:
```bash
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
@ -116,7 +116,9 @@ using GFramework.Cqrs.Extensions;
var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
`ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync``SendQueryAsync``PublishAsync``CreateStream`
`ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync``SendAsync``SendQueryAsync``PublishAsync``CreateStream`
如果你走标准 `GFramework.Core` 架构启动路径,`CqrsRuntimeModule` 会自动创建 runtime 并接线默认注册流程;只有在裸容器、测试宿主或自定义组合根里,才需要显式补齐 runtime、publisher 策略或额外程序集注册。
## 运行时行为
@ -126,6 +128,8 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
- 通知分发
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`
- notification publish 不存在 generated invoker 通道;它始终基于当前已注册的 `INotificationHandler<>` 集合和选定的 `INotificationPublisher` 策略执行。
- 默认 runtime 只消费一个 `INotificationPublisher`;如果容器里已经存在该注册,再调用 `UseNotificationPublisher(...)``UseNotificationPublisher<TPublisher>()``UseSequentialNotificationPublisher()``UseTaskWhenAllNotificationPublisher()` 会直接报错,而不是按“后注册覆盖前注册”处理。
- 内置 notification publisher 的推荐选择如下:
| 策略 | 推荐场景 | 执行顺序 | 失败语义 | 备注 |
@ -134,7 +138,7 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
| `TaskWhenAllNotificationPublisher` | 需要让全部处理器并行完成,并在结束后统一观察失败或取消 | 不保证顺序 | 不会在首个失败时停止其余处理器;会聚合最终异常或取消结果 | 更适合语义补齐,不是性能开关 |
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入仓库外的自定义策略或第三方策略 | 取决于具体实现 | 取决于具体实现 | 前者复用现成实例,后者让容器负责单例生命周期 |
- 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景。
- 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景,而不是把 publish 切成另一条 generated 或更快的分发通道
如果你需要显式保留默认顺序语义,也可以在组合根里直接声明:
@ -191,18 +195,20 @@ container.UseNotificationPublisher<MyCustomNotificationPublisher>();
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时registrar 会把对应 descriptor 元数据接线到 runtime 缓存。
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
- generated invoker 只覆盖 request 与 stream 两类单次分发元数据;`INotificationHandler<>` 仍然只参与 registry / fallback 注册,通知分发本身继续由 runtime 解析出的 handler 集合和 `INotificationPublisher` 策略决定。
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]``string[]` 两类 fallback 清单。
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。
- `CqrsReflectionFallbackAttribute` 可以多次声明,并同时承载 `Type[]``string[]` 两类 fallback 清单。
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker、空 fallback 元数据,或生成注册器整体不可用时,才会退回整程序集扫描。
- 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。
如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。
## 适用边界
- 这个包是默认实现,不是“纯契约包”。
- 这个包是默认实现,不是“纯契约包”;如果你只需要共享请求/处理器契约,请停在 `GeWuYou.GFramework.Cqrs.Abstractions`
- 处理器基类依赖 runtime 在分发前注入上下文,不适合脱离 dispatcher 直接手动实例化后调用。
- README 中的消息基类和 handler 基类位于 `GFramework.Cqrs`,接口契约位于 `GFramework.Cqrs.Abstractions`;最小示例通常需要同时引入这两个命名空间层级。
- 如果你的目标只是“先用起来”,优先沿用 `ArchitectureContext` / `IContextAware` 的统一入口;只有在需要更换通知策略、接入额外程序集或搭裸容器测试时,再显式配置组合根。
## 文档入口

View File

@ -5,6 +5,50 @@
围绕 `GFramework` 当前的双轨 CQRS 现状,继续完成以“去外部依赖、降低反射、收口公开入口”为目标的
CQRS 迁移与收敛。
## 归档使用说明
- 本文件只覆盖 `CQRS-REWRITE-RP-076``CQRS-REWRITE-RP-131` 的历史恢复材料,用于 boot 或人工恢复时快速定位旧阶段锚点。
- `RP-131` 是本归档的截止恢复点,不是当前 active 入口;继续恢复时应回到 active `todos/``traces/`,再从 archive 补读所需历史。
- 若只需要历史结论优先查本文件的阶段索引若需要当轮验证细节、worker 边界或判断过程,再跳到配套 archive trace。
## 归档截止点
- 截止恢复点:`CQRS-REWRITE-RP-131`
- 截止阶段benchmark 并发运行隔离入口
- 截止结论:
- benchmark 入口已支持 `--artifacts-suffix <suffix>`,并通过独立 host 工作目录隔离 BenchmarkDotNet 并发运行产物。
- 截止轮次的写面仍收敛在 `GFramework.Cqrs.Benchmarks` 单模块,没有重新扩散到 runtime、测试或 active tracking。
- 截止后的历史跳转:
- 需要 `RP-129``RP-131` 的 benchmark 隔离 / scoped-host / worker 波次细节:查看下方 `2026-05-11`
- 需要 `RP-123``RP-128` 的 PR review 收口与 stream/request benchmark 对齐:查看下方 `2026-05-09`
- 需要 `RP-113``RP-122` 的 notification publisher 与 request 热路径收敛:查看下方 `2026-05-08`
- 需要 `RP-101``RP-112` 的 benchmark 基线、PR review 收口与 bridge 之后的性能阶段:查看下方 `2026-05-07`
- 需要 `RP-076``RP-100` 的较早 generator gate / legacy bridge / benchmark 引入历史:查看下方 `2026-04-30``2026-05-04``2026-05-06`
## 阶段索引
- `RP-129` ~ `RP-131`
- 主题benchmark 多 worker 波次、stream scoped-host、并发运行隔离入口。
- 入口:`2026-05-11`
- `RP-123` ~ `RP-128`
- 主题:`PR #344` / `PR #345` review 收口stream lifetime 与 generated binding 对齐。
- 入口:`2026-05-09`
- `RP-113` ~ `RP-122`
- 主题notification publisher 公开策略、组合根配置、fan-out benchmark、request 零管道热路径缓存。
- 入口:`2026-05-08`
- `RP-101` ~ `RP-112`
- 主题request / stream benchmark 对照、性能门槛、`PR #339` ~ `PR #341` review 收口。
- 入口:`2026-05-07`
- `RP-076` ~ `RP-100`
- 主题active 入口历史收敛、generator contract gate、legacy Core CQRS bridge、benchmark 基础设施引入。
- 入口:`2026-04-30``2026-05-04``2026-05-06`
## 跳转约定
- 想找某个恢复点,直接搜索 `CQRS-REWRITE-RP-xxx`
- 想找某一轮最小验证,优先搜索该阶段下的“验证”或“本轮验证”。
- 想找下一批从哪里接续,优先搜索该阶段下的“下一恢复点”或“当前下一步”。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-131`

View File

@ -1,5 +1,38 @@
# CQRS 重写迁移追踪
## 归档导航
- 本 trace 归档承接 `CQRS-REWRITE-RP-076``CQRS-REWRITE-RP-131` 的执行细节,作用是给 boot / 人工恢复提供“按阶段回看”的证据面,而不是重新充当 active trace。
- 若只需要知道历史阶段做了什么、下一步曾指向哪里,先看对应日期块的首段与“下一恢复点”。
- 若只需要验证锚点优先看各阶段下的“本轮验证”“本轮验证结果”或“验证RP-xxx”。
## 快速索引
- `2026-05-11`
- `RP-129` ~ `RP-131`
- 主题benchmark 多 worker 波次、stream scoped-host、并发运行隔离入口。
- `2026-05-09`
- `RP-123` ~ `RP-128`
- 主题:`PR #344` / `PR #345` review 收口stream lifetime 与 generated binding 对齐。
- `2026-05-08`
- `RP-101` ~ `RP-122`
- 主题notification publisher、request 热路径、benchmark 对照与 review 收口。
- `2026-05-07`
- `RP-093` ~ `RP-100`
- 主题legacy bridge、stream pipeline seam、性能门槛与 benchmark 对照。
- `2026-05-06`
- `RP-083` ~ `RP-091`
- 主题generator gate 回归、benchmark 基础设施、startup / invoker 对照。
- `2026-05-04``2026-04-30`
- `RP-076` ~ `RP-082`
- 主题active 入口历史收敛、generator contract gate 与早期 PR 锚点整理。
## 跳转约定
- 搜索 `CQRS-REWRITE-RP-xxx` 可直接定位单个恢复点。
- 搜索“下一恢复点”可快速看到该阶段向后衔接的建议。
- 搜索“本轮结论”可快速判断该阶段是否值得继续细读。
## 2026-05-11
### 阶段benchmark 并发运行隔离入口CQRS-REWRITE-RP-131

View File

@ -12,57 +12,99 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-132`
- 恢复点编号:`CQRS-REWRITE-RP-134`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #347`
- 当前 PR 锚点:`PR #348`
- 当前结论:
- 已用 `$gframework-pr-review` 重新抓取并复核 `PR #347` 的 latest-head review当前仍成立的代码问题已收口到
`GFramework.Cqrs.Benchmarks` 单模块与 `ai-plan/public/cqrs-rewrite/**` 恢复入口。
- `Program.cs` 现在按“任意已生效的 artifacts 隔离配置”而不是“仅命令行 `--artifacts-suffix`”决定是否重启隔离宿主;
同时新增“目标宿主目录不得等于或嵌套在当前宿主输出目录内”的防守式校验,避免 `host/host/...` 递归膨胀。
- `RequestLifetimeBenchmarks``StreamLifetimeBenchmarks``Scoped` 路径改为复用单个 scoped runtime /
dispatcher只在每次 benchmark 调用时显式创建并释放真实 DI scope避免把 runtime 构造常量成本混进生命周期矩阵。
- `ScopedBenchmarkContainer` 已补齐只读适配语义与作用域租约的 XML 合同说明,避免 PR review 再次停留在“公开成员文档不完整”。
- active tracking / trace 已完成瘦身:历史长流水迁移到 `archive/` 新文件,当前 active 入口只保留恢复点、风险、
权威验证与下一步。
- 本轮按 `$gframework-batch-boot` 协调多波 non-conflicting subagent基线固定为
`origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`
- 本轮停止继续扩 batch 的主信号是 `reviewability / context-budget`,不是 `50` 文件阈值;
自然停点时累计 branch diff 约为 `12 files`,仍明显低于阈值。
- CQRS runtime / tests 侧已补齐并提交:
- `CqrsNotificationPublisherTests` 锁定“多 publisher 报错”与“单 dispatcher 内 publisher 缓存复用”
- `CqrsGeneratedRequestInvokerProviderTests``CqrsHandlerRegistrar` 收口 generated descriptor 的异常枚举、
坏元数据与重复 pair 回退契约
- `CqrsDispatcherCacheTests` 锁定 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
- benchmark 侧已补齐并提交:
- `RequestStartupBenchmarks``Mediator` startup 对照
- `StreamStartupBenchmarks`
- `NotificationStartupBenchmarks`
- `GFramework.Cqrs.Benchmarks/README.md` 的 current coverage / gap 收口
- 文档与恢复入口侧已补齐并提交:
- `GFramework.Cqrs/README.md`
- `docs/zh-CN/core/cqrs.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
- 当前 `PR #348` latest-head review 再次复核后:
- 跳过 `NotificationLifetimeBenchmarks.HandlerLifetime``[GenerateEnumExtensions]` 建议,原因是仓库没有“所有枚举统一生成扩展”的约定,且 benchmark 局部枚举不在该能力的强制范围内
- 接受并修复 `NotificationLifetimeBenchmarks` 的 scoped 容器释放与公开 XML 文档缺口
- 接受并修复 `CqrsHandlerRegistrar` 对 generated descriptor 的“先去重后校验”缺陷,并补回归测试锁定“首条无效、后条有效”的同键场景
- 接受并修复 generated descriptor 校验对 `MethodInfo` 使用 `ReferenceEquals` 的过严比较,改为按方法语义等价匹配
- 当前尚未提交的收尾切片仅剩:
- `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
- `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs`
- `GFramework.Cqrs/README.md`
- `docs/zh-CN/core/command.md`
- `docs/zh-CN/core/query.md`
- 本 tracking / trace 文件本身
## 当前活跃事实
- 当前分支:`feat/cqrs-optimization`
- 当前 PR`PR #347`
- 当前 PR`PR #348`
- 当前写面:
- `GFramework.Cqrs.Benchmarks/Program.cs`
- `GFramework.Cqrs.Benchmarks/README.md`
- `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/ScopedBenchmarkContainer.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/NotificationStartupBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs`
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs`
- `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`
- `GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs`
- `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs`
- `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
- `GFramework.Cqrs/README.md`
- `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`
- `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
- `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-migration-tracking-history-through-rp131.md`
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-migration-trace-history-through-rp131.md`
- `docs/zh-CN/core/command.md`
- `docs/zh-CN/core/cqrs.md`
- `docs/zh-CN/core/query.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 当前基线:
- `RequestLifetimeBenchmarks.SendRequest_GFrameworkCqrs` short-job 当前约为
`Singleton 52.69 ns / 32 B``Transient 57.88 ns / 56 B``Scoped 144.72 ns / 368 B`
- `StreamLifetimeBenchmarks.Stream_GFramework*` short-job 当前约为
`Scoped + FirstItem 266.7~267.0 ns / 792 B`
`Scoped + DrainAll 331.6~332.2 ns / 856 B`
- 两条并发 smoke 均已落到独立的
`BenchmarkDotNet.Artifacts/pr347-req-scoped/host/...`
`BenchmarkDotNet.Artifacts/pr347-stream-scoped/host/...`
- 本轮 batch 启动前,分支相对基线的累计 diff 为 `0 files / 0 lines`
- 当前自然停点时,累计 diff 约为 `12 files`
- 本轮新增 benchmark smoke 结果:
- `RequestStartupBenchmarks`
- `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
- `ColdStart_Mediator 110.867 us / 57872 B`
- `ColdStart_MediatR 679.103 us / 606256 B`
- `StreamStartupBenchmarks`
- `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
- `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
- `ColdStart_MediatR 933.87 us / 678992 B`
- `NotificationStartupBenchmarks`
- `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
- `ColdStart_Mediator 136.08 us / 62512 B`
- `ColdStart_MediatR 1.379 ms / 719056 B`
## 当前风险
- `Program.cs` 的“嵌套目标目录保护”只覆盖当前宿主目录与隔离宿主目录关系;若后续再扩展更多自定义 artifacts 入口,
仍需保持同一层防守式校验,避免配置分叉。
- `ScopedBenchmarkContainer` 现在明确禁止重叠 active scope若后续 benchmark 引入同一 runtime 的并行枚举或嵌套调用,
需要新的宿主模型,不能直接突破当前只读适配器的约束。
- 本轮 benchmark 结果仍是 `job short + 1 iteration` smoke用于证明路径正确与相对量级不应用作稳定性能结论。
- `NotificationLifetimeBenchmarks` 当前已跑完整默认作业,但还没并入提交;若继续新开 batch未提交面会明显降低可审查性。
- `RequestStartup` 的提交 `8990749d` 连带带入了 `CqrsDispatcherCacheTests.cs`;虽然两条切片均有效且已验证通过,但提交边界不再严格对应单个 ownership slice。
- startup 与 lifetime benchmark 的默认作业结果已足以证明路径与相对量级,但 `Initialization_*` 与少量 short-run 结果仍不应直接当成稳定排序结论。
## 最近权威验证
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
- 结果:通过,`Passed: 4, Failed: 0`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix pr347-req-scoped --filter "*RequestLifetimeBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:`Singleton 52.69 ns / 32 B``Transient 57.88 ns / 56 B``Scoped 144.72 ns / 368 B`
@ -72,9 +114,9 @@ CQRS 迁移与收敛。
## 下一推荐步骤
1. 再次运行 `$gframework-pr-review` 复核 `PR #347` latest-head open thread 是否已随本轮 head 收敛
2. 若 review 已清空,继续留在 `GFramework.Cqrs.Benchmarks` 单模块推进下一批 benchmark 对照,而不是立即扩散到 runtime
3. 若 review 仍保留 benchmark 相关线程,优先区分 stale 与新增结论,再决定是否需要新的 scoped-host 或 artifacts 入口修补
1. 先提交当前未提交的 `NotificationLifetime + registration fallback tests + CQRS/legacy docs` 收尾切片,回收工作树到干净状态
2. 再次运行 `$gframework-pr-review`,复核 `PR #348` latest-head open thread 是否已随着本轮多波 head 收敛
3. 若继续扩 benchmark优先从 `GFramework.Cqrs.Benchmarks/README.md` 已明确列出的 gap 中选下一个单文件切片,而不是继续扩大 shared infra 改动面
## 活跃文档

View File

@ -7,6 +7,29 @@ SPDX-License-Identifier: Apache-2.0
## 2026-05-11
### 阶段PR #348 latest-head review 再收口CQRS-REWRITE-RP-134
- 重新执行 `$gframework-pr-review` 抓取当前分支 `feat/cqrs-optimization` 对应的 `PR #348`
- 本轮 latest-head open AI thread 复核结论:
- `NotificationLifetimeBenchmarks.HandlerLifetime``[GenerateEnumExtensions]` 仍判定为泛化误报
- 仓库没有“产品/benchmark 枚举默认都启用该特性”的现行约定
- benchmark 项目也未接入 `GFramework.Core.SourceGenerators.Abstractions`,不应为局部对照枚举平白扩大 generator 依赖面
- `NotificationLifetimeBenchmarks``_scopedContainer` 释放缺口与公开 benchmark API 的 XML 契约缺口仍成立,接受修复
- `CqrsHandlerRegistrar` 中 generated descriptor 的“先去重后校验”缺陷仍成立,接受修复并补测试
- `CqrsHandlerRegistrar``MethodInfo` 使用 `ReferenceEquals` 的过严比较仍成立,接受修复
- active tracking / trace 的当前 PR 锚点仍停留在 `PR #347`,接受同步到 `PR #348`
- 本轮主线程实施:
- `NotificationLifetimeBenchmarks`
- `Cleanup()``_scopedContainer` 一并交给 `BenchmarkCleanupHelper.DisposeAll(...)`
- 为公开 benchmark 方法与公开 handler 方法补齐缺失的 `<returns>` / `<param>` XML 契约
- `CqrsHandlerRegistrar`
- request / stream generated descriptor 预热路径改为“先 `TryValidate...`,后写入 `registeredKeys`
- descriptor 对齐判断从 `ReferenceEquals(resolvedDescriptor.InvokerMethod, ...)` 调整为 `resolvedDescriptor.InvokerMethod.Equals(...)`
- `CqrsGeneratedRequestInvokerProviderTests`
- 新增 request / stream 两个回归用例,锁定“首条同键 descriptor 无效、后条有效时,仍应接受后条有效 generated descriptor”
- `ai-plan/public/cqrs-rewrite/**`
- 将 active tracking / trace 的当前 PR 锚点同步到 `PR #348`
### 阶段PR #347 latest-head review 收口CQRS-REWRITE-RP-132
- 使用 `$gframework-pr-review` 重新抓取当前分支 `feat/cqrs-optimization` 对应的 `PR #347`
@ -64,3 +87,55 @@ SPDX-License-Identifier: Apache-2.0
### 当前下一步
- 推送本轮变更后,重新运行 `$gframework-pr-review`,确认 `PR #347` 的 latest-head open thread 是否已随着新 head 收敛。
### 阶段:多波 batch 收口与 benchmark / docs 扩面CQRS-REWRITE-RP-133
- 按 `$gframework-batch-boot` 启动多波 non-conflicting subagent基线固定为
`origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`
- 启动前分支累计 diff 为 `0 files / 0 lines`;自然停点时累计 branch diff 约为 `12 files`
- 主线程把 stop decision 明确交给 `reviewability / context-budget`,没有在仍有文件预算时继续机械追到 `50 files`
- 本轮 accepted delegated scope
- runtime / tests
- `CqrsNotificationPublisherTests`:补“多 publisher 报错”与“publisher 缓存复用”回归
- `CqrsGeneratedRequestInvokerProviderTests` + `CqrsHandlerRegistrar`:补 generated descriptor 坏元数据、异常枚举、重复 pair 回退契约
- `CqrsDispatcherCacheTests`:补 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
- `CqrsRegistrationServiceTests`:补稳定程序集键 fallback 到 `AssemblyName.Name` / `ToString()` 的回归
- benchmarks
- `RequestStartupBenchmarks`:补 `Mediator` startup 对照
- `StreamStartupBenchmarks`
- `NotificationStartupBenchmarks`
- `NotificationLifetimeBenchmarks`
- `GFramework.Cqrs.Benchmarks/README.md`:收口当前 coverage / gap / smoke 解释边界
- docs / recovery
- `GFramework.Cqrs/README.md`
- `docs/zh-CN/core/cqrs.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- `docs/zh-CN/core/command.md`
- `docs/zh-CN/core/query.md`
- `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
- 本轮权威验证:
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix notif-lifetime --filter "*NotificationLifetimeBenchmarks*"`
- 本轮 benchmark 结果摘要:
- `RequestStartupBenchmarks`
- `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
- `ColdStart_Mediator 110.867 us / 57872 B`
- `ColdStart_MediatR 679.103 us / 606256 B`
- `StreamStartupBenchmarks`
- `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
- `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
- `ColdStart_MediatR 933.87 us / 678992 B`
- `NotificationStartupBenchmarks`
- `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
- `ColdStart_Mediator 136.08 us / 62512 B`
- `ColdStart_MediatR 1.379 ms / 719056 B`
- `NotificationLifetimeBenchmarks`
- `Singleton``GFramework 295.48 ns / 360 B``MediatR 77.99 ns / 288 B`
- `Scoped``GFramework 410.92 ns / 640 B``MediatR 213.49 ns / 632 B`
- `Transient``GFramework 311.21 ns / 416 B``MediatR 74.36 ns / 288 B`
- 当前收尾判断:
- branch diff 仍远低于 `50` 文件阈值,但 active 未提交面与 benchmark 运行输出已经足够构成自然 stop boundary
- 下一步不继续扩 batch先提交当前收尾切片并回到干净工作树再按 PR review 结果决定后续波次

View File

@ -109,25 +109,45 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline也不会额外补上下文桥接链路。
## 兼容入口和 CQRS bridge 的关系
这里可以把旧命令路径理解成“保留旧 API、内部接到新 runtime”
- 对调用方来说,`SendCommand(...)` / `SendCommandAsync(...)` 仍然是旧命令入口
- 对运行时来说,标准 `Architecture` 路径会把这些旧命令包装成内部 bridge request再交给 `ICqrsRuntime`
- 对处理过程来说,命令最终会复用当前 CQRS 的 request pipeline 与上下文注入链路,而不是维持一套完全独立的分发栈
因此,兼容入口的意义主要是降低迁移成本,而不是鼓励新模块继续围绕旧执行器设计。
`IContextAware` 对象内,通常直接通过扩展使用:
```csharp
using GFramework.Core.Extensions;
```
## 什么时候还应该用旧命令
## 什么时候继续保留旧命令
- 你在维护既有 `Core.Command` 代码
- 你的调用链已经依赖旧 `CommandExecutor`
- 当前改动目标是局部修复,不值得同时做 CQRS 迁移
- 你需要保持现有命令类型、调用入口或测试夹具不变,只希望它们在标准架构下继续工作
## 什么时候该切到 CQRS
这类场景的重点是“让存量代码继续跑”,而不是把旧命令体系当成新模块默认入口。
下面这些场景更适合新 CQRS runtime
## 什么时候该开始迁移
如果出现下面这些信号,说明更适合把命令迁到新 CQRS
- 需要 request / notification / stream 的统一模型
- 需要 pipeline behaviors
- 需要 handler registry 生成器
- 你正在写新的业务模块,而不是维护历史命令代码
- 你希望命令处理逻辑直接落在 `AbstractCommandHandler<,>` 等 CQRS handler 上,而不是继续扩展 `AbstractCommand*`
- 你需要让命令和查询、通知共用同一套注册与调试路径
一个简单判断方法:
- 继续保留旧路径:为了兼容已有 `Command` 类型和调用链
- 迁移到 CQRS为了给新功能建立统一 request model而不是继续扩大 legacy 面积
迁移后常见写法见:[cqrs](./cqrs.md)

View File

@ -118,6 +118,7 @@ var playerId = await architecture.Context.SendRequestAsync(
- 已解析处理器按容器顺序逐个执行
- 首个处理器抛出异常时立即停止后续分发
- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`
- 默认 runtime 只消费一个 `INotificationPublisher`;如果容器里已经存在该注册,再调用 `UseNotificationPublisher*` 系列扩展会直接报错,而不是按“后注册覆盖前注册”处理
如果你需要在组合根里明确表达“为什么选这条策略”,可以按下面的矩阵判断:
@ -125,7 +126,7 @@ var playerId = await architecture.Context.SendRequestAsync(
| --- | --- | --- | --- | --- |
| `UseSequentialNotificationPublisher()` | 需要保持容器顺序,且希望首个失败立即停止 | 保证按容器顺序执行 | 首个处理器异常会中断后续处理器 | 这也是默认回退策略 |
| `UseTaskWhenAllNotificationPublisher()` | 需要让全部处理器并行完成,再统一观察异常或取消 | 不保证顺序 | 不会在首个失败时中断其余处理器;全部结束后统一暴露结果 | 更适合语义补齐,不是性能优化开关 |
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入自定义或第三方 publisher 策略 | 取决于实现 | 取决于实现 | 前者复用现成实例,后者让容器负责单例生命周期 |
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入自定义或第三方 publisher 策略 | 取决于实现 | 取决于实现 | 前者复用现成实例,后者让容器负责单例生命周期;两者都要求容器此前尚未注册 `INotificationPublisher` |
如果你想在组合根里显式保留默认顺序语义,也可以直接写成:
@ -216,11 +217,12 @@ protected override void OnInitialize()
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
3. 当生成注册器同时暴露 generated request invoker provider 时runtime 会把 request/response 类型对对应的 descriptor 预先接线到 dispatcher 缓存,后续请求分发优先消费这些 generated request invoker 元数据
4. 当生成注册器同时暴露 generated stream invoker provider 时runtime 会以同样方式优先消费 stream request 对应的 generated stream invoker descriptor只有当前类型对未命中时才回退到既有反射 stream binding
5. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射绑定已命中的不兼容元数据会直接抛出异常
6. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
7. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
8. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
9. 同一程序集按稳定键去重,避免重复注册
5. generated invoker 只覆盖 request 与 stream 两类单次分发元数据notification handler 仍通过已注册的 `INotificationHandler<>` 集合和选定的 `INotificationPublisher` 参与分发,不存在对应的 generated notification invoker 通道
6. 生成注册器不可用时记录告警并回退到反射路径;只有“未命中 generated descriptor”才会走反射 binding 创建,已成功登记到缓存的类型对不会再回退到另一条 generated 通道
7. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
8. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
9. 只有 fallback 元数据为空、仍是旧版空 marker 语义,或生成注册器整体不可用时,才会回到整程序集反射扫描
10. 同一程序集按稳定键去重,避免重复注册
换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是生成注册器负责能静态表达的部分fallback 只补它覆盖不到的 handler。
@ -231,7 +233,7 @@ protected override void OnInitialize()
- stream invoker provider / descriptor
- 面向 `CreateStream(...)` 触发的流式请求分发
两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。
两者的共同点都是“优先消费 generated invoker 元数据,未命中时保留既有反射绑定作为兜底”,而不是要求业务侧切换到另一套 runtime 入口。通知发布不在这组 generated invoker 能力里;它始终沿用 runtime 解析出的 handler 集合与当前 publisher 策略。
对接入方来说,更关键的 reader-facing 语义是:安装 `Cqrs.SourceGenerators` 后,不要求“所有 handler 都能被生成代码直接引用”才有收益。
即使仍有 fallbackruntime 也会先消费 generated registry再只对剩余 handler 做定向补扫;只有旧版 marker 语义或空 fallback 元数据才会退回整程序集扫描。

View File

@ -85,6 +85,17 @@ var count = this.SendQuery(
在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`
因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。
只有在你直接 `new QueryExecutor()``new AsyncQueryExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;这时异步查询也不会进入统一 CQRS pipeline。
## 兼容入口和 CQRS bridge 的关系
旧查询页面的重点不是再引入一套新执行模型,而是说明兼容入口现在如何接到 CQRS runtime
- `SendQuery(...)` / `SendQueryAsync(...)` 仍然是面向存量代码的旧 API
- 标准 `Architecture` 路径会把旧查询包装成内部 bridge request再交给 `ICqrsRuntime`
- 这让旧查询对象在不改调用方式的前提下,也能共享当前 CQRS 的 pipeline、handler 调度和上下文注入语义
如果你依赖的是 direct executor 测试或隔离运行,那么仍要把它看成 legacy 路径,而不是完整的新 CQRS 使用方式。
`IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
@ -97,10 +108,11 @@ using GFramework.Core.Extensions;
- 你在维护现有 `Core.Query` 代码
- 当前代码已经建立在旧查询执行器之上
- 你只想修正局部行为,不想顺手迁移整条调用链
- 你需要保留现有 `AbstractQuery*` 类型与测试入口,只要求标准架构下继续复用统一 runtime
## 什么时候改用 CQRS 查询
如果你正在写新的读取路径,优先考虑:
如果你正在写新的读取路径,或者已经需要统一读写模型,优先考虑:
- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>`
- `AbstractQueryHandler<TQuery, TResponse>`
@ -108,4 +120,9 @@ using GFramework.Core.Extensions;
原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。
可以按下面的判断来选:
- 继续保留旧路径:为了兼容已有 `Query` 类型、旧执行器或局部修复场景
- 迁移到 CQRS为了把新的读取能力纳入统一 request model而不是继续扩大 legacy 查询面
继续阅读:[cqrs](./cqrs.md)

View File

@ -40,6 +40,7 @@ runtime 在注册 handlers 时优先走静态注册表;当运行时合同允
这意味着运行时会先使用生成注册器完成可静态表达的映射;对 request 与 stream 分发来说,也会优先消费 generated invoker
descriptor。只有当前类型对没有 generated metadata或 registry / fallback 无法覆盖时,才继续回到既有反射 binding 或补扫路径,而不是退回整程序集盲扫。
如果这些 fallback handlers 本身仍可直接引用,生成器会优先发射 `typeof(...)` 形式的 fallback 元数据;当 runtime 允许同一程序集声明多个 fallback 特性实例时mixed 场景也会拆成 `Type` 元数据和字符串元数据两段,进一步减少 runtime 再做字符串类型名回查的成本。
这里的 generated invoker 只覆盖 `IRequestHandler<,>``IStreamRequestHandler<,>``INotificationHandler<>` 仍然只参与 registry / fallback 注册;通知分发本身继续由 runtime 解析出的 handler 集合和 `INotificationPublisher` 策略决定。
## 最小接入路径
@ -87,9 +88,10 @@ RegisterCqrsHandlersFromAssemblies(
2. 优先激活生成的 `ICqrsHandlerRegistry`
3. 若生成注册器同时提供 request invoker provider / descriptorregistrar 会把这些 request invoker 元数据预先登记到 dispatcher 缓存
4. 若生成注册器同时提供 stream invoker provider / descriptorruntime 也会优先消费对应的 generated stream invoker 元数据;未命中时仍回退到既有反射 stream binding
5. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
6. 若存在 `CqrsReflectionFallbackAttribute`,优先按其中携带的 `Type` 或类型名补扫剩余 handler若元数据为空或只保留 marker 语义,则退回整程序集补扫
7. 同一程序集按稳定键去重,避免重复注册
5. generated invoker provider 不是独立入口;它只是让 dispatcher 在已知 `requestType + responseType` 类型对时优先命中编译期 descriptor未命中时仍保持原有 runtime 分发入口
6. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
7. 若存在 `CqrsReflectionFallbackAttribute`,优先按其中携带的 `Type` 或类型名补扫剩余 handler只有元数据为空、只保留 marker 语义,或 registry 整体不可用时,才退回整程序集补扫
8. 同一程序集按稳定键去重,避免重复注册
这个行为由
[运行时注册流程测试](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs)
@ -127,6 +129,8 @@ RegisterCqrsHandlersFromAssemblies(
- 其余场景统一回退到字符串元数据,避免 mixed 场景漏注册
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
`fallback` 在这里表示“补齐生成注册器没有直接接线的剩余 handler”不是“生成器一出现就重新扫描整个程序集”。只要 attribute 里已经带了明确 `Type` 或类型名runtime 就会先走这份定向清单。
## 生成策略层级
把这个生成器理解成“静态注册 or 整程序集扫描”的二选一,会低估它的收益。当前策略实际上分成四层:
@ -141,6 +145,7 @@ RegisterCqrsHandlersFromAssemblies(
- 只有前面几层都无法覆盖的剩余 handler才交给 `CqrsReflectionFallbackAttribute`
这意味着安装生成器后,并不要求“所有 handler 都可直接引用”才有收益。很多只能部分静态表达的项目,仍然可以把大部分注册路径前移到编译期,再对少数复杂类型做定向补扫。
其中 request / stream 的 generated invoker descriptor 只在前两类 runtime seam 同时存在、且当前 handler 能安全生成静态 invoker 时才会出现;否则对应请求仍然走已存在的反射 binding 创建路径,不会影响 registry 本身继续工作。
## 哪些场景通常不会直接退回整程序集扫描