mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 20:38:58 +08:00
docs(cqrs): 收口批处理剩余文档与追踪
- 新增 NotificationLifetime 基准并补充验证结果 - 更新 CQRS README 与 legacy Command/Query 迁移说明 - 补充 registration fallback 回归测试并同步 ai-plan 恢复点
This commit is contained in:
parent
f650bc5776
commit
babd132e81
@ -0,0 +1,315 @@
|
||||
// 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(_container, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler,作为不同生命周期矩阵下的 publish 额外开销 baseline。
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ValueTask PublishNotification_Baseline()
|
||||
{
|
||||
return _baselineHandler.Handle(_notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 发布 notification。
|
||||
/// </summary>
|
||||
[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>
|
||||
[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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,13 +38,16 @@
|
||||
- `Messaging/StreamStartupBenchmarks.cs`
|
||||
- `Initialization` 与 `ColdStart` 两组下,`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR`
|
||||
- 其中 `ColdStart` 的边界是“新宿主 + 首个元素命中”,不是完整枚举整个 stream
|
||||
- notification
|
||||
- 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`
|
||||
|
||||
## 最小使用方式
|
||||
|
||||
@ -95,6 +98,5 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
|
||||
|
||||
- 当前没有 stream 版的 NuGet `Mediator` source-generated concrete path 对照;stream steady-state、lifetime、startup 现在都只覆盖 `GFramework.Cqrs` 与 `MediatR`
|
||||
- 当前没有 request 生命周期下的 NuGet `Mediator` compile-time lifetime 矩阵;`RequestLifetimeBenchmarks` 只覆盖 `GFramework.Cqrs` 与 `MediatR`
|
||||
- 当前没有 notification startup / cold-start benchmark;notification 只覆盖 steady-state 单处理器、生命周期、固定 `4 handler` fan-out
|
||||
- 当前没有 notification fan-out 的生命周期矩阵;`NotificationFanOutBenchmarks` 只覆盖固定 `4 handler` 的已装配宿主
|
||||
- 当前没有 stream pipeline benchmark;现有 pipeline coverage 仅限 request
|
||||
|
||||
@ -87,17 +87,125 @@ internal sealed class CqrsRegistrationServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 <see cref="Assembly.FullName" /> 缺失时,协调器会退化到 <see cref="AssemblyName.Name" /> 作为稳定程序集键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Fallback_To_Simple_Name_When_Full_Name_Is_Missing()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
var firstAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
|
||||
assemblyDisplayName: "DisplayName-A");
|
||||
var secondAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback",
|
||||
assemblyDisplayName: "DisplayName-B");
|
||||
IEnumerable<Assembly>? registeredAssemblies = null;
|
||||
|
||||
registrar
|
||||
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
|
||||
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
|
||||
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([firstAssembly.Object]);
|
||||
service.RegisterHandlers([secondAssembly.Object]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Once);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
|
||||
var debugMessages = logger.Logs
|
||||
.Where(static log => log.Level == LogLevel.Debug)
|
||||
.Select(static log => log.Message)
|
||||
.ToArray();
|
||||
Assert.That(debugMessages, Has.Length.EqualTo(1));
|
||||
Assert.That(debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.SimpleNameFallback"));
|
||||
Assert.That(debugMessages[0], Does.Not.Contain("DisplayName-B"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 <see cref="Assembly.FullName" /> 与 <see cref="AssemblyName.Name" /> 均缺失时,
|
||||
/// 协调器会退化到 <see cref="object.ToString" /> 结果作为稳定程序集键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Fallback_To_ToString_When_Full_Name_And_Simple_Name_Are_Missing()
|
||||
{
|
||||
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
|
||||
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
|
||||
const string assemblyDisplayName = "GFramework.Cqrs.Tests.ToStringFallback";
|
||||
var firstAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: null,
|
||||
assemblyDisplayName: assemblyDisplayName);
|
||||
var secondAssembly = CreateAssembly(
|
||||
assemblyFullName: null,
|
||||
assemblySimpleName: null,
|
||||
assemblyDisplayName: assemblyDisplayName);
|
||||
IEnumerable<Assembly>? registeredAssemblies = null;
|
||||
|
||||
registrar
|
||||
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
|
||||
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
|
||||
|
||||
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
|
||||
|
||||
service.RegisterHandlers([firstAssembly.Object]);
|
||||
service.RegisterHandlers([secondAssembly.Object]);
|
||||
|
||||
registrar.Verify(
|
||||
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
|
||||
Times.Once);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
|
||||
var debugMessages = logger.Logs
|
||||
.Where(static log => log.Level == LogLevel.Debug)
|
||||
.Select(static log => log.Message)
|
||||
.ToArray();
|
||||
Assert.That(debugMessages, Has.Length.EqualTo(1));
|
||||
Assert.That(debugMessages[0], Does.Contain(assemblyDisplayName));
|
||||
Assert.That(debugMessages[0], Does.Contain("already registered"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个带稳定程序集键的程序集 mock,用于模拟不同 <see cref="Assembly" /> 实例表示同一程序集的场景。
|
||||
/// </summary>
|
||||
/// <param name="assemblyFullName">要返回的程序集完整名称。</param>
|
||||
/// <returns>配置好完整名称的程序集 mock。</returns>
|
||||
private static Mock<Assembly> CreateAssembly(string assemblyFullName)
|
||||
{
|
||||
return CreateAssembly(assemblyFullName, assemblySimpleName: null, assemblyDisplayName: assemblyFullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个可配置程序集元数据退化路径的程序集 mock,用于验证稳定程序集键的回退顺序。
|
||||
/// </summary>
|
||||
/// <param name="assemblyFullName">要返回的程序集完整名称;为 <see langword="null" /> 时模拟缺失完整名称。</param>
|
||||
/// <param name="assemblySimpleName">要返回的程序集简单名称;为 <see langword="null" /> 时模拟缺失简单名称。</param>
|
||||
/// <param name="assemblyDisplayName">当需要退化到 <see cref="object.ToString" /> 时返回的显示名称。</param>
|
||||
/// <returns>配置好程序集元数据的程序集 mock。</returns>
|
||||
private static Mock<Assembly> CreateAssembly(string? assemblyFullName, string? assemblySimpleName, string assemblyDisplayName)
|
||||
{
|
||||
var assembly = new Mock<Assembly>();
|
||||
var assemblyName = new AssemblyName();
|
||||
assembly
|
||||
.SetupGet(static currentAssembly => currentAssembly.FullName)
|
||||
.Returns(assemblyFullName);
|
||||
assemblyName.Name = assemblySimpleName;
|
||||
assembly
|
||||
.Setup(static currentAssembly => currentAssembly.GetName())
|
||||
.Returns(assemblyName);
|
||||
assembly
|
||||
.Setup(static currentAssembly => currentAssembly.ToString())
|
||||
.Returns(assemblyDisplayName);
|
||||
|
||||
return assembly;
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ dotnet add package GeWuYou.GFramework.Cqrs
|
||||
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
|
||||
```
|
||||
|
||||
如果你希望减少处理器注册时的反射扫描,再额外安装:
|
||||
如果你希望把可静态表达的 handler 注册与 request / stream invoker 元数据前移到编译期,再额外安装:
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
|
||||
@ -116,7 +116,9 @@ using GFramework.Cqrs.Extensions;
|
||||
var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInput("Alice")));
|
||||
```
|
||||
|
||||
在 `ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync`、`SendQueryAsync`、`PublishAsync` 和 `CreateStream`。
|
||||
在 `ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync`、`SendAsync`、`SendQueryAsync`、`PublishAsync` 和 `CreateStream`。
|
||||
|
||||
如果你走标准 `GFramework.Core` 架构启动路径,`CqrsRuntimeModule` 会自动创建 runtime 并接线默认注册流程;只有在裸容器、测试宿主或自定义组合根里,才需要显式补齐 runtime、publisher 策略或额外程序集注册。
|
||||
|
||||
## 运行时行为
|
||||
|
||||
@ -126,6 +128,8 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
|
||||
- 通知分发
|
||||
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
|
||||
- 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`。
|
||||
- notification publish 不存在 generated invoker 通道;它始终基于当前已注册的 `INotificationHandler<>` 集合和选定的 `INotificationPublisher` 策略执行。
|
||||
- 默认 runtime 只消费一个 `INotificationPublisher`;如果容器里已经存在该注册,再调用 `UseNotificationPublisher(...)`、`UseNotificationPublisher<TPublisher>()`、`UseSequentialNotificationPublisher()` 或 `UseTaskWhenAllNotificationPublisher()` 会直接报错,而不是按“后注册覆盖前注册”处理。
|
||||
- 内置 notification publisher 的推荐选择如下:
|
||||
|
||||
| 策略 | 推荐场景 | 执行顺序 | 失败语义 | 备注 |
|
||||
@ -134,7 +138,7 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
|
||||
| `TaskWhenAllNotificationPublisher` | 需要让全部处理器并行完成,并在结束后统一观察失败或取消 | 不保证顺序 | 不会在首个失败时停止其余处理器;会聚合最终异常或取消结果 | 更适合语义补齐,不是性能开关 |
|
||||
| `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` | 需要接入仓库外的自定义策略或第三方策略 | 取决于具体实现 | 取决于具体实现 | 前者复用现成实例,后者让容器负责单例生命周期 |
|
||||
|
||||
- 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景。
|
||||
- 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景,而不是把 publish 切成另一条 generated 或更快的分发通道。
|
||||
|
||||
如果你需要显式保留默认顺序语义,也可以在组合根里直接声明:
|
||||
|
||||
@ -191,18 +195,20 @@ container.UseNotificationPublisher<MyCustomNotificationPublisher>();
|
||||
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
|
||||
- 当生成注册器同时暴露 generated request invoker provider 或 generated stream invoker provider 时,registrar 会把对应 descriptor 元数据接线到 runtime 缓存。
|
||||
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
|
||||
- generated invoker 只覆盖 request 与 stream 两类单次分发元数据;`INotificationHandler<>` 仍然只参与 registry / fallback 注册,通知分发本身继续由 runtime 解析出的 handler 集合和 `INotificationPublisher` 策略决定。
|
||||
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
|
||||
- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
|
||||
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。
|
||||
- `CqrsReflectionFallbackAttribute` 可以多次声明,并同时承载 `Type[]` 与 `string[]` 两类 fallback 清单。
|
||||
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker、空 fallback 元数据,或生成注册器整体不可用时,才会退回整程序集扫描。
|
||||
- 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。
|
||||
|
||||
如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。
|
||||
|
||||
## 适用边界
|
||||
|
||||
- 这个包是默认实现,不是“纯契约包”。
|
||||
- 这个包是默认实现,不是“纯契约包”;如果你只需要共享请求/处理器契约,请停在 `GeWuYou.GFramework.Cqrs.Abstractions`。
|
||||
- 处理器基类依赖 runtime 在分发前注入上下文,不适合脱离 dispatcher 直接手动实例化后调用。
|
||||
- README 中的消息基类和 handler 基类位于 `GFramework.Cqrs`,接口契约位于 `GFramework.Cqrs.Abstractions`;最小示例通常需要同时引入这两个命名空间层级。
|
||||
- 如果你的目标只是“先用起来”,优先沿用 `ArchitectureContext` / `IContextAware` 的统一入口;只有在需要更换通知策略、接入额外程序集或搭裸容器测试时,再显式配置组合根。
|
||||
|
||||
## 文档入口
|
||||
|
||||
|
||||
@ -12,57 +12,94 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-132`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-133`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前 PR 锚点:`PR #347`
|
||||
- 当前结论:
|
||||
- 已用 `$gframework-pr-review` 重新抓取并复核 `PR #347` 的 latest-head review,当前仍成立的代码问题已收口到
|
||||
`GFramework.Cqrs.Benchmarks` 单模块与 `ai-plan/public/cqrs-rewrite/**` 恢复入口。
|
||||
- `Program.cs` 现在按“任意已生效的 artifacts 隔离配置”而不是“仅命令行 `--artifacts-suffix`”决定是否重启隔离宿主;
|
||||
同时新增“目标宿主目录不得等于或嵌套在当前宿主输出目录内”的防守式校验,避免 `host/host/...` 递归膨胀。
|
||||
- `RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 的 `Scoped` 路径改为复用单个 scoped runtime /
|
||||
dispatcher,只在每次 benchmark 调用时显式创建并释放真实 DI scope,避免把 runtime 构造常量成本混进生命周期矩阵。
|
||||
- `ScopedBenchmarkContainer` 已补齐只读适配语义与作用域租约的 XML 合同说明,避免 PR review 再次停留在“公开成员文档不完整”。
|
||||
- active tracking / trace 已完成瘦身:历史长流水迁移到 `archive/` 新文件,当前 active 入口只保留恢复点、风险、
|
||||
权威验证与下一步。
|
||||
- 本轮按 `$gframework-batch-boot` 协调多波 non-conflicting subagent,基线固定为
|
||||
`origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`。
|
||||
- 本轮停止继续扩 batch 的主信号是 `reviewability / context-budget`,不是 `50` 文件阈值;
|
||||
自然停点时累计 branch diff 约为 `12 files`,仍明显低于阈值。
|
||||
- CQRS runtime / tests 侧已补齐并提交:
|
||||
- `CqrsNotificationPublisherTests` 锁定“多 publisher 报错”与“单 dispatcher 内 publisher 缓存复用”
|
||||
- `CqrsGeneratedRequestInvokerProviderTests` 与 `CqrsHandlerRegistrar` 收口 generated descriptor 的异常枚举、
|
||||
坏元数据与重复 pair 回退契约
|
||||
- `CqrsDispatcherCacheTests` 锁定 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
|
||||
- benchmark 侧已补齐并提交:
|
||||
- `RequestStartupBenchmarks` 的 `Mediator` startup 对照
|
||||
- `StreamStartupBenchmarks`
|
||||
- `NotificationStartupBenchmarks`
|
||||
- `GFramework.Cqrs.Benchmarks/README.md` 的 current coverage / gap 收口
|
||||
- 文档与恢复入口侧已补齐并提交:
|
||||
- `GFramework.Cqrs/README.md`
|
||||
- `docs/zh-CN/core/cqrs.md`
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
|
||||
- 当前尚未提交的收尾切片仅剩:
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs`
|
||||
- `GFramework.Cqrs/README.md`
|
||||
- `docs/zh-CN/core/command.md`
|
||||
- `docs/zh-CN/core/query.md`
|
||||
- 本 tracking / trace 文件本身
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前分支:`feat/cqrs-optimization`
|
||||
- 当前 PR:`PR #347`
|
||||
- 当前写面:
|
||||
- `GFramework.Cqrs.Benchmarks/Program.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/README.md`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/ScopedBenchmarkContainer.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/NotificationLifetimeBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/NotificationStartupBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/StreamStartupBenchmarks.cs`
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs`
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs`
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs`
|
||||
- `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
|
||||
- `GFramework.Cqrs/README.md`
|
||||
- `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`
|
||||
- `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-migration-tracking-history-through-rp131.md`
|
||||
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-migration-trace-history-through-rp131.md`
|
||||
- `docs/zh-CN/core/command.md`
|
||||
- `docs/zh-CN/core/cqrs.md`
|
||||
- `docs/zh-CN/core/query.md`
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 当前基线:
|
||||
- `RequestLifetimeBenchmarks.SendRequest_GFrameworkCqrs` short-job 当前约为
|
||||
`Singleton 52.69 ns / 32 B`、`Transient 57.88 ns / 56 B`、`Scoped 144.72 ns / 368 B`
|
||||
- `StreamLifetimeBenchmarks.Stream_GFramework*` short-job 当前约为
|
||||
`Scoped + FirstItem 266.7~267.0 ns / 792 B`、
|
||||
`Scoped + DrainAll 331.6~332.2 ns / 856 B`
|
||||
- 两条并发 smoke 均已落到独立的
|
||||
`BenchmarkDotNet.Artifacts/pr347-req-scoped/host/...` 与
|
||||
`BenchmarkDotNet.Artifacts/pr347-stream-scoped/host/...`
|
||||
- 本轮 batch 启动前,分支相对基线的累计 diff 为 `0 files / 0 lines`
|
||||
- 当前自然停点时,累计 diff 约为 `12 files`
|
||||
- 本轮新增 benchmark smoke 结果:
|
||||
- `RequestStartupBenchmarks`
|
||||
- `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
|
||||
- `ColdStart_Mediator 110.867 us / 57872 B`
|
||||
- `ColdStart_MediatR 679.103 us / 606256 B`
|
||||
- `StreamStartupBenchmarks`
|
||||
- `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
|
||||
- `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
|
||||
- `ColdStart_MediatR 933.87 us / 678992 B`
|
||||
- `NotificationStartupBenchmarks`
|
||||
- `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
|
||||
- `ColdStart_Mediator 136.08 us / 62512 B`
|
||||
- `ColdStart_MediatR 1.379 ms / 719056 B`
|
||||
|
||||
## 当前风险
|
||||
|
||||
- `Program.cs` 的“嵌套目标目录保护”只覆盖当前宿主目录与隔离宿主目录关系;若后续再扩展更多自定义 artifacts 入口,
|
||||
仍需保持同一层防守式校验,避免配置分叉。
|
||||
- `ScopedBenchmarkContainer` 现在明确禁止重叠 active scope;若后续 benchmark 引入同一 runtime 的并行枚举或嵌套调用,
|
||||
需要新的宿主模型,不能直接突破当前只读适配器的约束。
|
||||
- 本轮 benchmark 结果仍是 `job short + 1 iteration` smoke,用于证明路径正确与相对量级,不应用作稳定性能结论。
|
||||
- `NotificationLifetimeBenchmarks` 当前已跑完整默认作业,但还没并入提交;若继续新开 batch,未提交面会明显降低可审查性。
|
||||
- `RequestStartup` 的提交 `8990749d` 连带带入了 `CqrsDispatcherCacheTests.cs`;虽然两条切片均有效且已验证通过,但提交边界不再严格对应单个 ownership slice。
|
||||
- startup 与 lifetime benchmark 的默认作业结果已足以证明路径与相对量级,但 `Initialization_*` 与少量 short-run 结果仍不应直接当成稳定排序结论。
|
||||
|
||||
## 最近权威验证
|
||||
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
|
||||
- 结果:通过,`Passed: 4, Failed: 0`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix pr347-req-scoped --filter "*RequestLifetimeBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Singleton 52.69 ns / 32 B`、`Transient 57.88 ns / 56 B`、`Scoped 144.72 ns / 368 B`
|
||||
@ -72,9 +109,9 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 下一推荐步骤
|
||||
|
||||
1. 再次运行 `$gframework-pr-review` 复核 `PR #347` latest-head open thread 是否已随本轮 head 收敛。
|
||||
2. 若 review 已清空,继续留在 `GFramework.Cqrs.Benchmarks` 单模块推进下一批 benchmark 对照,而不是立即扩散到 runtime。
|
||||
3. 若 review 仍保留 benchmark 相关线程,优先区分 stale 与新增结论,再决定是否需要新的 scoped-host 或 artifacts 入口修补。
|
||||
1. 先提交当前未提交的 `NotificationLifetime + registration fallback tests + CQRS/legacy docs` 收尾切片,回收工作树到干净状态。
|
||||
2. 再次运行 `$gframework-pr-review`,复核 `PR #347` latest-head open thread 是否已随着本轮多波 head 收敛。
|
||||
3. 若继续扩 benchmark,优先从 `GFramework.Cqrs.Benchmarks/README.md` 已明确列出的 gap 中选下一个单文件切片,而不是继续扩大 shared infra 改动面。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
|
||||
@ -64,3 +64,55 @@ SPDX-License-Identifier: Apache-2.0
|
||||
### 当前下一步
|
||||
|
||||
- 推送本轮变更后,重新运行 `$gframework-pr-review`,确认 `PR #347` 的 latest-head open thread 是否已随着新 head 收敛。
|
||||
|
||||
### 阶段:多波 batch 收口与 benchmark / docs 扩面(CQRS-REWRITE-RP-133)
|
||||
|
||||
- 按 `$gframework-batch-boot` 启动多波 non-conflicting subagent,基线固定为
|
||||
`origin/main @ 3b2e6899d5ffdcfb634b28f3846f57528fbf9196 (2026-05-11T12:25:00+08:00)`。
|
||||
- 启动前分支累计 diff 为 `0 files / 0 lines`;自然停点时累计 branch diff 约为 `12 files`。
|
||||
- 主线程把 stop decision 明确交给 `reviewability / context-budget`,没有在仍有文件预算时继续机械追到 `50 files`。
|
||||
- 本轮 accepted delegated scope:
|
||||
- runtime / tests
|
||||
- `CqrsNotificationPublisherTests`:补“多 publisher 报错”与“publisher 缓存复用”回归
|
||||
- `CqrsGeneratedRequestInvokerProviderTests` + `CqrsHandlerRegistrar`:补 generated descriptor 坏元数据、异常枚举、重复 pair 回退契约
|
||||
- `CqrsDispatcherCacheTests`:补 request / stream pipeline presence、executor cache 与上下文重新注入组合分支
|
||||
- `CqrsRegistrationServiceTests`:补稳定程序集键 fallback 到 `AssemblyName.Name` / `ToString()` 的回归
|
||||
- benchmarks
|
||||
- `RequestStartupBenchmarks`:补 `Mediator` startup 对照
|
||||
- `StreamStartupBenchmarks`
|
||||
- `NotificationStartupBenchmarks`
|
||||
- `NotificationLifetimeBenchmarks`
|
||||
- `GFramework.Cqrs.Benchmarks/README.md`:收口当前 coverage / gap / smoke 解释边界
|
||||
- docs / recovery
|
||||
- `GFramework.Cqrs/README.md`
|
||||
- `docs/zh-CN/core/cqrs.md`
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- `docs/zh-CN/core/command.md`
|
||||
- `docs/zh-CN/core/query.md`
|
||||
- `ai-plan/public/cqrs-rewrite/archive/**` 顶部导航与跳转约定
|
||||
- 本轮权威验证:
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsRegistrationServiceTests"`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix notif-lifetime --filter "*NotificationLifetimeBenchmarks*"`
|
||||
- 本轮 benchmark 结果摘要:
|
||||
- `RequestStartupBenchmarks`
|
||||
- `ColdStart_GFrameworkCqrs 61.648 us / 25336 B`
|
||||
- `ColdStart_Mediator 110.867 us / 57872 B`
|
||||
- `ColdStart_MediatR 679.103 us / 606256 B`
|
||||
- `StreamStartupBenchmarks`
|
||||
- `ColdStart_GFrameworkReflection 71.13 us / 25504 B`
|
||||
- `ColdStart_GFrameworkGenerated 82.12 us / 28280 B`
|
||||
- `ColdStart_MediatR 933.87 us / 678992 B`
|
||||
- `NotificationStartupBenchmarks`
|
||||
- `ColdStart_GFrameworkCqrs 85.09 us / 24752 B`
|
||||
- `ColdStart_Mediator 136.08 us / 62512 B`
|
||||
- `ColdStart_MediatR 1.379 ms / 719056 B`
|
||||
- `NotificationLifetimeBenchmarks`
|
||||
- `Singleton`:`GFramework 295.48 ns / 360 B`,`MediatR 77.99 ns / 288 B`
|
||||
- `Scoped`:`GFramework 410.92 ns / 640 B`,`MediatR 213.49 ns / 632 B`
|
||||
- `Transient`:`GFramework 311.21 ns / 416 B`,`MediatR 74.36 ns / 288 B`
|
||||
- 当前收尾判断:
|
||||
- branch diff 仍远低于 `50` 文件阈值,但 active 未提交面与 benchmark 运行输出已经足够构成自然 stop boundary
|
||||
- 下一步不继续扩 batch,先提交当前收尾切片并回到干净工作树,再按 PR review 结果决定后续波次
|
||||
|
||||
@ -109,25 +109,45 @@ var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3)
|
||||
这意味着历史命令调用链在不改调用方式的前提下,也会复用同一套 pipeline 与上下文注入语义。
|
||||
只有在你直接 `new CommandExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;此时不会注入统一 pipeline,也不会额外补上下文桥接链路。
|
||||
|
||||
## 兼容入口和 CQRS bridge 的关系
|
||||
|
||||
这里可以把旧命令路径理解成“保留旧 API、内部接到新 runtime”:
|
||||
|
||||
- 对调用方来说,`SendCommand(...)` / `SendCommandAsync(...)` 仍然是旧命令入口
|
||||
- 对运行时来说,标准 `Architecture` 路径会把这些旧命令包装成内部 bridge request,再交给 `ICqrsRuntime`
|
||||
- 对处理过程来说,命令最终会复用当前 CQRS 的 request pipeline 与上下文注入链路,而不是维持一套完全独立的分发栈
|
||||
|
||||
因此,兼容入口的意义主要是降低迁移成本,而不是鼓励新模块继续围绕旧执行器设计。
|
||||
|
||||
在 `IContextAware` 对象内,通常直接通过扩展使用:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Extensions;
|
||||
```
|
||||
|
||||
## 什么时候还应该用旧命令
|
||||
## 什么时候继续保留旧命令
|
||||
|
||||
- 你在维护既有 `Core.Command` 代码
|
||||
- 你的调用链已经依赖旧 `CommandExecutor`
|
||||
- 当前改动目标是局部修复,不值得同时做 CQRS 迁移
|
||||
- 你需要保持现有命令类型、调用入口或测试夹具不变,只希望它们在标准架构下继续工作
|
||||
|
||||
## 什么时候该切到 CQRS
|
||||
这类场景的重点是“让存量代码继续跑”,而不是把旧命令体系当成新模块默认入口。
|
||||
|
||||
下面这些场景更适合新 CQRS runtime:
|
||||
## 什么时候该开始迁移
|
||||
|
||||
如果出现下面这些信号,说明更适合把命令迁到新 CQRS:
|
||||
|
||||
- 需要 request / notification / stream 的统一模型
|
||||
- 需要 pipeline behaviors
|
||||
- 需要 handler registry 生成器
|
||||
- 你正在写新的业务模块,而不是维护历史命令代码
|
||||
- 你希望命令处理逻辑直接落在 `AbstractCommandHandler<,>` 等 CQRS handler 上,而不是继续扩展 `AbstractCommand*`
|
||||
- 你需要让命令和查询、通知共用同一套注册与调试路径
|
||||
|
||||
一个简单判断方法:
|
||||
|
||||
- 继续保留旧路径:为了兼容已有 `Command` 类型和调用链
|
||||
- 迁移到 CQRS:为了给新功能建立统一 request model,而不是继续扩大 legacy 面积
|
||||
|
||||
迁移后常见写法见:[cqrs](./cqrs.md)
|
||||
|
||||
@ -85,6 +85,17 @@ var count = this.SendQuery(
|
||||
|
||||
在标准架构启动路径中,这些兼容入口底层同样会转到统一 `ICqrsRuntime`。
|
||||
因此历史查询对象仍保持原始 `SendQuery(...)` / `SendQueryAsync(...)` 用法,但会共享新版 request pipeline 与上下文注入链路。
|
||||
只有在你直接 `new QueryExecutor()` 或 `new AsyncQueryExecutor()` 做隔离测试,且没有提供 `ICqrsRuntime` 时,才会回退到 legacy 直接执行;这时异步查询也不会进入统一 CQRS pipeline。
|
||||
|
||||
## 兼容入口和 CQRS bridge 的关系
|
||||
|
||||
旧查询页面的重点不是再引入一套新执行模型,而是说明兼容入口现在如何接到 CQRS runtime:
|
||||
|
||||
- `SendQuery(...)` / `SendQueryAsync(...)` 仍然是面向存量代码的旧 API
|
||||
- 标准 `Architecture` 路径会把旧查询包装成内部 bridge request,再交给 `ICqrsRuntime`
|
||||
- 这让旧查询对象在不改调用方式的前提下,也能共享当前 CQRS 的 pipeline、handler 调度和上下文注入语义
|
||||
|
||||
如果你依赖的是 direct executor 测试或隔离运行,那么仍要把它看成 legacy 路径,而不是完整的新 CQRS 使用方式。
|
||||
|
||||
在 `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展:
|
||||
|
||||
@ -97,10 +108,11 @@ using GFramework.Core.Extensions;
|
||||
- 你在维护现有 `Core.Query` 代码
|
||||
- 当前代码已经建立在旧查询执行器之上
|
||||
- 你只想修正局部行为,不想顺手迁移整条调用链
|
||||
- 你需要保留现有 `AbstractQuery*` 类型与测试入口,只要求标准架构下继续复用统一 runtime
|
||||
|
||||
## 什么时候改用 CQRS 查询
|
||||
|
||||
如果你正在写新的读取路径,优先考虑:
|
||||
如果你正在写新的读取路径,或者已经需要统一读写模型,优先考虑:
|
||||
|
||||
- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>`
|
||||
- `AbstractQueryHandler<TQuery, TResponse>`
|
||||
@ -108,4 +120,9 @@ using GFramework.Core.Extensions;
|
||||
|
||||
原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。
|
||||
|
||||
可以按下面的判断来选:
|
||||
|
||||
- 继续保留旧路径:为了兼容已有 `Query` 类型、旧执行器或局部修复场景
|
||||
- 迁移到 CQRS:为了把新的读取能力纳入统一 request model,而不是继续扩大 legacy 查询面
|
||||
|
||||
继续阅读:[cqrs](./cqrs.md)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user