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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,39 +1,53 @@
# GFramework.Cqrs.Benchmarks # GFramework.Cqrs.Benchmarks
该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于持续比较运行时 dispatch、publish、cold-start 与后续 generator / pipeline 收口的成本变化 该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于在当前 HEAD 上复核 request、stream、notification 的 steady-state 与 startup 成本边界
## 目的 ## 目的
- 为 `GFramework.Cqrs` 建立独立于 NUnit 集成测试的 BenchmarkDotNet 基线 - 为 `GFramework.Cqrs` 提供独立于测试工程的 BenchmarkDotNet 复核入口
- 参考 `ai-libs/Mediator/benchmarks` 的场景组织方式,逐步补齐 request、notification、stream 与初始化成本对比 - 让 request、stream、notification 的热路径与 cold-start 变化有可重复的对照矩阵
- 为后续吸收 `Mediator` 的 dispatch 设计、fixture 组织和对比矩阵提供可重复验证入口 - 在不引入“未来已存在”假设的前提下,明确当前 benchmark 已覆盖什么、还没有覆盖什么
## 当前内容 ## 当前 coverage
- `Program.cs` 当前工程已经覆盖以下矩阵:
- benchmark 命令行入口
- `Messaging/Fixture.cs` - request steady-state
- 运行前输出并校验场景配置
- `Messaging/RequestBenchmarks.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 对比 - direct handler、默认 `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path、`MediatR`
- `Messaging/RequestLifetimeBenchmarks.cs` - `Messaging/RequestLifetimeBenchmarks.cs`
- `Singleton / Scoped / Transient` 三类 handler 生命周期下direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比;其中 `Scoped` 通过真实 request 级作用域宿主执行,不再把 scoped handler 退化为根容器解析 - `Singleton / Scoped / Transient` 三类 handler 生命周期下baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
- `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` - `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `0 / 1 / 4` 个 pipeline 行为下baseline、默认 generated-provider 宿主接线的 `GFramework.Cqrs` runtime 与 `MediatR`
- `Messaging/RequestStartupBenchmarks.cs`
- `Initialization``ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度
- `Messaging/RequestInvokerBenchmarks.cs` - `Messaging/RequestInvokerBenchmarks.cs`
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比 - baseline、`GFramework.Cqrs` reflection request binding、`GFramework.Cqrs` generated request invoker、`MediatR`
- `Messaging/StreamInvokerBenchmarks.cs` - request startup
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径 - `Messaging/RequestStartupBenchmarks.cs`
- `Messaging/NotificationBenchmarks.cs` - `Initialization``ColdStart` 两组下,`GFramework.Cqrs`、NuGet `Mediator``MediatR`
- `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比 - stream steady-state
- `Messaging/NotificationFanOutBenchmarks.cs`
- fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比
- `Messaging/StreamingBenchmarks.cs` - `Messaging/StreamingBenchmarks.cs`
- direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径 - 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_*" 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 ```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 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_*" 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 矩阵对照 - 当前没有 stream 版的 NuGet `Mediator` source-generated concrete path 对照stream steady-state、lifetime、startup 现在都只覆盖 `GFramework.Cqrs``MediatR`
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景 - 当前没有 request 生命周期下的 NuGet `Mediator` compile-time lifetime 矩阵;`RequestLifetimeBenchmarks` 只覆盖 `GFramework.Cqrs``MediatR`
- 当前没有 notification fan-out 的生命周期矩阵;`NotificationFanOutBenchmarks` 只覆盖固定 `4 handler` 的已装配宿主
- 当前没有 stream pipeline benchmark现有 pipeline coverage 仅限 request

View File

@ -309,6 +309,60 @@ internal sealed class CqrsDispatcherCacheTests
}); });
} }
/// <summary>
/// 验证同一 request dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
/// </summary>
[Test]
public async Task Dispatcher_Should_Create_Request_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
{
var requestBindings = GetCacheField("RequestDispatchBindings");
var behaviorType = typeof(IPipelineBehavior<DispatcherPipelineCacheRequest, int>);
using var zeroBehaviorContainer = CreateFrozenContainer(
new DispatcherCacheFixtureOptions
{
IncludeRequestPipelineCacheBehavior = false
});
var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer);
var behaviorContext = new ArchitectureContext(_container!);
var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext);
var behaviorDispatcher = GetDispatcherFromContext(behaviorContext);
await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
var bindingAfterZeroBehaviorDispatch = GetPairCacheValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int));
var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int),
1);
await behaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest());
var bindingAfterBehaviorDispatch = GetPairCacheValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int));
var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int),
1);
Assert.Multiple(() =>
{
Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null);
Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch));
Assert.That(executorAfterZeroBehaviorDispatch, Is.Null);
Assert.That(executorAfterBehaviorDispatch, Is.Not.Null);
AssertRequestBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false);
AssertRequestBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true);
});
}
/// <summary> /// <summary>
/// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。 /// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。
/// </summary> /// </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> /// <summary>
/// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。 /// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。
/// </summary> /// </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> /// <summary>
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler /// 验证缓存的 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> /// <summary>
/// 通过反射读取 dispatcher 的静态缓存对象。 /// 通过反射读取 dispatcher 的静态缓存对象。
/// </summary> /// </summary>
@ -679,10 +954,11 @@ internal sealed class CqrsDispatcherCacheTests
/// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器, /// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器,
/// 用于验证 dispatcher 的实例级缓存不会跨容器共享。 /// 用于验证 dispatcher 的实例级缓存不会跨容器共享。
/// </summary> /// </summary>
private static MicrosoftDiContainer CreateFrozenContainer() /// <param name="options">控制当前隔离容器要启用哪些可选 pipeline 行为的配置。</param>
private static MicrosoftDiContainer CreateFrozenContainer(DispatcherCacheFixtureOptions? options = null)
{ {
var container = new MicrosoftDiContainer(); var container = new MicrosoftDiContainer();
ConfigureDispatcherCacheFixture(container); ConfigureDispatcherCacheFixture(container, options);
container.Freeze(); container.Freeze();
return container; return container;
@ -692,16 +968,44 @@ internal sealed class CqrsDispatcherCacheTests
/// 组装当前 fixture 依赖的 CQRS 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。 /// 组装当前 fixture 依赖的 CQRS 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。
/// </summary> /// </summary>
/// <param name="container">待补齐 CQRS 注册的目标容器。</param> /// <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)
{
options ??= new DispatcherCacheFixtureOptions();
if (options.IncludeRequestPipelineCacheBehavior)
{ {
container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>(); container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
}
if (options.IncludeRequestPipelineContextRefreshBehavior)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>(); container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
}
if (options.IncludeRequestPipelineOrderBehaviors)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>(); container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>(); container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
}
if (options.IncludeStreamPipelineCacheBehavior)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>(); container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>();
}
if (options.IncludeStreamPipelineContextRefreshBehavior)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>(); container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>();
}
if (options.IncludeStreamPipelineOrderBehaviors)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>(); container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>(); container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>();
}
CqrsTestRuntime.RegisterHandlers( CqrsTestRuntime.RegisterHandlers(
container, container,

View File

@ -448,6 +448,166 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Assert.That(results, Is.EqualTo([3, 4])); 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> /// <summary>
/// 模拟返回实例 request invoker 方法的 generated registry。 /// 模拟返回实例 request invoker 方法的 generated registry。
/// </summary> /// </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> /// <summary>
/// 创建带有 generated request invoker registry 元数据的程序集替身。 /// 创建带有 generated request invoker registry 元数据的程序集替身。
/// </summary> /// </summary>

View File

@ -91,6 +91,70 @@ internal sealed class CqrsNotificationPublisherTests
Assert.That(handler.ObservedContext, Is.SameAs(architectureContext.Object)); 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> /// <summary>
/// 验证内置 `TaskWhenAll` 发布器会继续调度所有处理器,而不是沿用默认顺序发布器的失败即停语义。 /// 验证内置 `TaskWhenAll` 发布器会继续调度所有处理器,而不是沿用默认顺序发布器的失败即停语义。
/// </summary> /// </summary>
@ -262,6 +326,11 @@ internal sealed class CqrsNotificationPublisherTests
/// </summary> /// </summary>
public bool WasCalled { get; private set; } public bool WasCalled { get; private set; }
/// <summary>
/// 获取当前发布器累计执行发布的次数。
/// </summary>
public int PublishCallCount { get; private set; }
/// <summary> /// <summary>
/// 记录当前发布器已被调用,并继续按当前顺序执行所有处理器。 /// 记录当前发布器已被调用,并继续按当前顺序执行所有处理器。
/// </summary> /// </summary>
@ -275,6 +344,7 @@ internal sealed class CqrsNotificationPublisherTests
where TNotification : INotification where TNotification : INotification
{ {
WasCalled = true; WasCalled = true;
PublishCallCount++;
foreach (var handler in context.Handlers) foreach (var handler in context.Handlers)
{ {

View File

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

View File

@ -16,6 +16,13 @@ namespace GFramework.Cqrs.Internal;
/// </summary> /// </summary>
internal static class CqrsHandlerRegistrar 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被回收后会重新分析而不会被静态缓存永久钉住。 // 若程序集来自 collectible AssemblyLoadContext被回收后会重新分析而不会被静态缓存永久钉住。
private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache = private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
@ -321,8 +328,49 @@ internal static class CqrsHandlerRegistrar
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource) if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
return; 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( CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
descriptorEntry.RequestType, descriptorEntry.RequestType,
descriptorEntry.ResponseType, descriptorEntry.ResponseType,
@ -376,8 +424,49 @@ internal static class CqrsHandlerRegistrar
if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource) if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource)
return; 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( CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor(
descriptorEntry.RequestType, descriptorEntry.RequestType,
descriptorEntry.ResponseType, 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> /// <summary>
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。 /// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
/// </summary> /// </summary>

View File

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

View File

@ -5,6 +5,50 @@
围绕 `GFramework` 当前的双轨 CQRS 现状,继续完成以“去外部依赖、降低反射、收口公开入口”为目标的 围绕 `GFramework` 当前的双轨 CQRS 现状,继续完成以“去外部依赖、降低反射、收口公开入口”为目标的
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` - 恢复点编号:`CQRS-REWRITE-RP-131`

View File

@ -1,5 +1,38 @@
# CQRS 重写迁移追踪 # 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 ## 2026-05-11
### 阶段benchmark 并发运行隔离入口CQRS-REWRITE-RP-131 ### 阶段benchmark 并发运行隔离入口CQRS-REWRITE-RP-131

View File

@ -12,57 +12,99 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-132` - 恢复点编号:`CQRS-REWRITE-RP-134`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #347` - 当前 PR 锚点:`PR #348`
- 当前结论: - 当前结论:
- 已用 `$gframework-pr-review` 重新抓取并复核 `PR #347` 的 latest-head review当前仍成立的代码问题已收口到 - 本轮按 `$gframework-batch-boot` 协调多波 non-conflicting subagent基线固定为
`GFramework.Cqrs.Benchmarks` 单模块与 `ai-plan/public/cqrs-rewrite/**` 恢复入口。 `origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`
- `Program.cs` 现在按“任意已生效的 artifacts 隔离配置”而不是“仅命令行 `--artifacts-suffix`”决定是否重启隔离宿主; - 本轮停止继续扩 batch 的主信号是 `reviewability / context-budget`,不是 `50` 文件阈值;
同时新增“目标宿主目录不得等于或嵌套在当前宿主输出目录内”的防守式校验,避免 `host/host/...` 递归膨胀。 自然停点时累计 branch diff 约为 `12 files`,仍明显低于阈值。
- `RequestLifetimeBenchmarks``StreamLifetimeBenchmarks``Scoped` 路径改为复用单个 scoped runtime / - CQRS runtime / tests 侧已补齐并提交:
dispatcher只在每次 benchmark 调用时显式创建并释放真实 DI scope避免把 runtime 构造常量成本混进生命周期矩阵。 - `CqrsNotificationPublisherTests` 锁定“多 publisher 报错”与“单 dispatcher 内 publisher 缓存复用”
- `ScopedBenchmarkContainer` 已补齐只读适配语义与作用域租约的 XML 合同说明,避免 PR review 再次停留在“公开成员文档不完整”。 - `CqrsGeneratedRequestInvokerProviderTests``CqrsHandlerRegistrar` 收口 generated descriptor 的异常枚举、
- active tracking / trace 已完成瘦身:历史长流水迁移到 `archive/` 新文件,当前 active 入口只保留恢复点、风险、 坏元数据与重复 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` - 当前分支:`feat/cqrs-optimization`
- 当前 PR`PR #347` - 当前 PR`PR #348`
- 当前写面: - 当前写面:
- `GFramework.Cqrs.Benchmarks/Program.cs`
- `GFramework.Cqrs.Benchmarks/README.md` - `GFramework.Cqrs.Benchmarks/README.md`
- `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs` - `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs` - `GFramework.Cqrs.Benchmarks/Messaging/NotificationStartupBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/ScopedBenchmarkContainer.cs` - `GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs`
- `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.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/todos/cqrs-rewrite-migration-tracking.md`
- `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.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/todos/cqrs-rewrite-migration-tracking-history-through-rp131.md`
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-migration-trace-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 当前约为 - 本轮 batch 启动前,分支相对基线的累计 diff 为 `0 files / 0 lines`
`Singleton 52.69 ns / 32 B``Transient 57.88 ns / 56 B``Scoped 144.72 ns / 368 B` - 当前自然停点时,累计 diff 约为 `12 files`
- `StreamLifetimeBenchmarks.Stream_GFramework*` short-job 当前约为 - 本轮新增 benchmark smoke 结果:
`Scoped + FirstItem 266.7~267.0 ns / 792 B` - `RequestStartupBenchmarks`
`Scoped + DrainAll 331.6~332.2 ns / 856 B` - `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
- 两条并发 smoke 均已落到独立的 - `ColdStart_Mediator 110.867 us / 57872 B`
`BenchmarkDotNet.Artifacts/pr347-req-scoped/host/...` - `ColdStart_MediatR 679.103 us / 606256 B`
`BenchmarkDotNet.Artifacts/pr347-stream-scoped/host/...` - `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 入口, - `NotificationLifetimeBenchmarks` 当前已跑完整默认作业,但还没并入提交;若继续新开 batch未提交面会明显降低可审查性。
仍需保持同一层防守式校验,避免配置分叉。 - `RequestStartup` 的提交 `8990749d` 连带带入了 `CqrsDispatcherCacheTests.cs`;虽然两条切片均有效且已验证通过,但提交边界不再严格对应单个 ownership slice。
- `ScopedBenchmarkContainer` 现在明确禁止重叠 active scope若后续 benchmark 引入同一 runtime 的并行枚举或嵌套调用, - startup 与 lifetime benchmark 的默认作业结果已足以证明路径与相对量级,但 `Initialization_*` 与少量 short-run 结果仍不应直接当成稳定排序结论。
需要新的宿主模型,不能直接突破当前只读适配器的约束。
- 本轮 benchmark 结果仍是 `job short + 1 iteration` smoke用于证明路径正确与相对量级不应用作稳定性能结论。
## 最近权威验证 ## 最近权威验证
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`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` - `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` - 备注:`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 收敛 1. 先提交当前未提交的 `NotificationLifetime + registration fallback tests + CQRS/legacy docs` 收尾切片,回收工作树到干净状态
2. 若 review 已清空,继续留在 `GFramework.Cqrs.Benchmarks` 单模块推进下一批 benchmark 对照,而不是立即扩散到 runtime 2. 再次运行 `$gframework-pr-review`,复核 `PR #348` latest-head open thread 是否已随着本轮多波 head 收敛
3. 若 review 仍保留 benchmark 相关线程,优先区分 stale 与新增结论,再决定是否需要新的 scoped-host 或 artifacts 入口修补 3. 若继续扩 benchmark优先从 `GFramework.Cqrs.Benchmarks/README.md` 已明确列出的 gap 中选下一个单文件切片,而不是继续扩大 shared infra 改动面
## 活跃文档 ## 活跃文档

View File

@ -7,6 +7,29 @@ SPDX-License-Identifier: Apache-2.0
## 2026-05-11 ## 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 ### 阶段PR #347 latest-head review 收口CQRS-REWRITE-RP-132
- 使用 `$gframework-pr-review` 重新抓取当前分支 `feat/cqrs-optimization` 对应的 `PR #347` - 使用 `$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 收敛。 - 推送本轮变更后,重新运行 `$gframework-pr-review`,确认 `PR #347` 的 latest-head open thread 是否已随着新 head 收敛。
### 阶段:多波 batch 收口与 benchmark / docs 扩面CQRS-REWRITE-RP-133
- 按 `$gframework-batch-boot` 启动多波 non-conflicting subagent基线固定为
`origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`
- 启动前分支累计 diff 为 `0 files / 0 lines`;自然停点时累计 branch diff 约为 `12 files`
- 主线程把 stop decision 明确交给 `reviewability / context-budget`,没有在仍有文件预算时继续机械追到 `50 files`
- 本轮 accepted delegated scope
- runtime / tests
- `CqrsNotificationPublisherTests`:补“多 publisher 报错”与“publisher 缓存复用”回归
- `CqrsGeneratedRequestInvokerProviderTests` + `CqrsHandlerRegistrar`:补 generated descriptor 坏元数据、异常枚举、重复 pair 回退契约
- `CqrsDispatcherCacheTests`:补 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
- `CqrsRegistrationServiceTests`:补稳定程序集键 fallback 到 `AssemblyName.Name` / `ToString()` 的回归
- benchmarks
- `RequestStartupBenchmarks`:补 `Mediator` startup 对照
- `StreamStartupBenchmarks`
- `NotificationStartupBenchmarks`
- `NotificationLifetimeBenchmarks`
- `GFramework.Cqrs.Benchmarks/README.md`:收口当前 coverage / gap / smoke 解释边界
- docs / recovery
- `GFramework.Cqrs/README.md`
- `docs/zh-CN/core/cqrs.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- `docs/zh-CN/core/command.md`
- `docs/zh-CN/core/query.md`
- `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
- 本轮权威验证:
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix notif-lifetime --filter "*NotificationLifetimeBenchmarks*"`
- 本轮 benchmark 结果摘要:
- `RequestStartupBenchmarks`
- `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
- `ColdStart_Mediator 110.867 us / 57872 B`
- `ColdStart_MediatR 679.103 us / 606256 B`
- `StreamStartupBenchmarks`
- `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
- `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
- `ColdStart_MediatR 933.87 us / 678992 B`
- `NotificationStartupBenchmarks`
- `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
- `ColdStart_Mediator 136.08 us / 62512 B`
- `ColdStart_MediatR 1.379 ms / 719056 B`
- `NotificationLifetimeBenchmarks`
- `Singleton``GFramework 295.48 ns / 360 B``MediatR 77.99 ns / 288 B`
- `Scoped``GFramework 410.92 ns / 640 B``MediatR 213.49 ns / 632 B`
- `Transient``GFramework 311.21 ns / 416 B``MediatR 74.36 ns / 288 B`
- 当前收尾判断:
- branch diff 仍远低于 `50` 文件阈值,但 active 未提交面与 benchmark 运行输出已经足够构成自然 stop boundary
- 下一步不继续扩 batch先提交当前收尾切片并回到干净工作树再按 PR review 结果决定后续波次

View File

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

View File

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

View File

@ -85,6 +85,17 @@ var count = this.SendQuery(
在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime` 在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`
因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。 因此历史查询对象仍保持原始 `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` 里的扩展: `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
@ -97,10 +108,11 @@ using GFramework.Core.Extensions;
- 你在维护现有 `Core.Query` 代码 - 你在维护现有 `Core.Query` 代码
- 当前代码已经建立在旧查询执行器之上 - 当前代码已经建立在旧查询执行器之上
- 你只想修正局部行为,不想顺手迁移整条调用链 - 你只想修正局部行为,不想顺手迁移整条调用链
- 你需要保留现有 `AbstractQuery*` 类型与测试入口,只要求标准架构下继续复用统一 runtime
## 什么时候改用 CQRS 查询 ## 什么时候改用 CQRS 查询
如果你正在写新的读取路径,优先考虑: 如果你正在写新的读取路径,或者已经需要统一读写模型,优先考虑:
- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>` - `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>`
- `AbstractQueryHandler<TQuery, TResponse>` - `AbstractQueryHandler<TQuery, TResponse>`
@ -108,4 +120,9 @@ using GFramework.Core.Extensions;
原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。 原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。
可以按下面的判断来选:
- 继续保留旧路径:为了兼容已有 `Query` 类型、旧执行器或局部修复场景
- 迁移到 CQRS为了把新的读取能力纳入统一 request model而不是继续扩大 legacy 查询面
继续阅读:[cqrs](./cqrs.md) 继续阅读:[cqrs](./cqrs.md)

View File

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