mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 20:38:58 +08:00
Merge pull request #348 from GeWuYou/feat/cqrs-optimization
Feat/Enhance CQRS benchmarks coverage and generated invoker descriptor validation
This commit is contained in:
commit
ef4d3d5ddf
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
430
GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs
Normal file
430
GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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` 的统一入口;只有在需要更换通知策略、接入额外程序集或搭裸容器测试时,再显式配置组合根。
|
||||
|
||||
## 文档入口
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 改动面。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
|
||||
@ -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 结果决定后续波次
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 都能被生成代码直接引用”才有收益。
|
||||
即使仍有 fallback,runtime 也会先消费 generated registry,再只对剩余 handler 做定向补扫;只有旧版 marker 语义或空 fallback 元数据才会退回整程序集扫描。
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 / descriptor,registrar 会把这些 request invoker 元数据预先登记到 dispatcher 缓存
|
||||
4. 若生成注册器同时提供 stream invoker provider / descriptor,runtime 也会优先消费对应的 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 本身继续工作。
|
||||
|
||||
## 哪些场景通常不会直接退回整程序集扫描
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user