diff --git a/GFramework.Core.Abstractions/README.md b/GFramework.Core.Abstractions/README.md index cbb44371..f4f5a709 100644 --- a/GFramework.Core.Abstractions/README.md +++ b/GFramework.Core.Abstractions/README.md @@ -26,7 +26,7 @@ | --- | --- | | `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、上下文、模块、服务模块、阶段监听、注册表基类与生命周期契约 | | `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` | 组件角色接口、优先级 / key 值对象、上下文感知约束与扩展边界 | -| `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及 `ICqrsRuntime` 这类新请求模型接线契约 | +| `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及面向 CQRS runtime 的兼容别名入口 | | `Events/` `Property/` `State/` `StateManagement/` | 事件总线、解绑对象、可绑定属性、状态机、Store / reducer / middleware 契约 | | `Coroutine/` `Time/` `Pause/` `Concurrency/` | 协程状态、时间源、暂停栈、键控异步锁和统计对象 | | `Resource/` `Pool/` `Logging/` `Localization/` | 资源句柄、对象池、日志、日志工厂、本地化表与格式化契约 | @@ -41,7 +41,7 @@ | 类型族 | 代表类型 | 阅读重点 | | --- | --- | --- | | `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、`IArchitectureContext`、`IServiceModule`、`KeyValueRegistryBase` | 看架构、上下文、模块装配与注册表基类边界 | -| `Command/` `Query/` `Cqrs/` | `ICommandExecutor`、`IAsyncQueryExecutor`、`ICqrsRuntime` | 看命令、查询与新请求模型的调用入口 | +| `Command/` `Query/` `Cqrs/` | `ICommandExecutor`、`IAsyncQueryExecutor`、`ICqrsRuntime` | 看旧命令 / 查询契约,以及 CQRS runtime 的兼容别名入口 | | `Events/` `Property/` `State/` `StateManagement/` | `IEventBus`、`IBindableProperty`、`IStateMachine`、`IStore` | 看事件分发、可绑定状态与 store 契约 | | `Coroutine/` `Time/` `Pause/` `Concurrency/` | `IYieldInstruction`、`ITimeProvider`、`IPauseStackManager`、`IAsyncKeyLockManager` | 看协程、时间源、暂停栈与并发协调能力 | | `Resource/` `Pool/` `Logging/` `Localization/` | `IResourceManager`、`IObjectPoolSystem`、`ILogger`、`ILocalizationManager` | 看资源、对象池、日志与本地化服务角色 | @@ -63,7 +63,11 @@ - 架构与模块入口:`IArchitecture`、`IArchitectureContext`、`IServiceModule` - 运行时基础设施:`IIocContainer`、`ILogger`、`IResourceManager`、`IConfigurationManager` - 状态与并发能力:`IStateMachine`、`IStore`、`IAsyncKeyLockManager`、`ITimeProvider` -- 迁移与组合边界:`ICommandExecutor`、`IQueryExecutor`、`ICqrsRuntime` +- 迁移与组合边界:`ICommandExecutor`、`IQueryExecutor`,以及旧命名空间下作为 compatibility alias 暴露的 `ICqrsRuntime` + +`GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 当前主要用于兼容旧命名空间引用。新代码应直接依赖 +`GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`,这样可以把请求模型、handler 和 runtime seam 保持在同一套 +CQRS 契约下。 ## 对应文档 diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index e0f204c7..5895e42f 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -306,8 +306,6 @@ public class ArchitectureContextTests public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() { const int workerCount = 8; - var workerStartupTimeout = TimeSpan.FromSeconds(5); - var firstResolutionTimeout = TimeSpan.FromSeconds(5); using var startGate = new ManualResetEventSlim(false); using var allowResolutionToComplete = new ManualResetEventSlim(false); using var workersReady = new CountdownEvent(workerCount); @@ -339,18 +337,11 @@ public class ArchitectureContextTests })) .ToArray(); - Assert.That( - workersReady.Wait(workerStartupTimeout), - Is.True, - "Expected all workers to be ready before releasing start gate."); - startGate.Set(); - - Assert.That( - SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), - Is.True, - "Expected at least one CQRS runtime resolution attempt."); - - allowResolutionToComplete.Set(); + ReleaseWorkersAfterFirstResolutionAttempt( + workersReady, + startGate, + allowResolutionToComplete, + () => Volatile.Read(ref resolutionCallCount) > 0); var responses = await Task.WhenAll(requests); @@ -365,7 +356,187 @@ public class ArchitectureContextTests Times.Exactly(requests.Length)); } + /// + /// 测试 CQRS runtime 在并发首次发布通知时只会从容器解析一次。 + /// + [Test] + public async Task PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() + { + const int workerCount = 8; + using var startGate = new ManualResetEventSlim(false); + using var allowResolutionToComplete = new ManualResetEventSlim(false); + using var workersReady = new CountdownEvent(workerCount); + var resolutionCallCount = 0; + var runtime = new Mock(MockBehavior.Strict); + var container = new Mock(MockBehavior.Strict); + + runtime.Setup(mockRuntime => mockRuntime.PublishAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(ValueTask.CompletedTask); + + container.Setup(mockContainer => mockContainer.Get()) + .Returns(() => + { + Interlocked.Increment(ref resolutionCallCount); + allowResolutionToComplete.Wait(); + return runtime.Object; + }); + + var context = new ArchitectureContext(container.Object); + var notifications = Enumerable.Range(0, workerCount) + .Select(_ => Task.Run(async () => + { + workersReady.Signal(); + startGate.Wait(); + await context.PublishAsync(new TestCqrsNotification()).ConfigureAwait(false); + })) + .ToArray(); + + ReleaseWorkersAfterFirstResolutionAttempt( + workersReady, + startGate, + allowResolutionToComplete, + () => Volatile.Read(ref resolutionCallCount) > 0); + + await Task.WhenAll(notifications).ConfigureAwait(false); + + Assert.That(resolutionCallCount, Is.EqualTo(1)); + container.Verify(mockContainer => mockContainer.Get(), Times.Once); + runtime.Verify( + mockRuntime => mockRuntime.PublishAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(notifications.Length)); + } + + /// + /// 测试 CQRS runtime 在并发首次创建流时只会从容器解析一次。 + /// + [Test] + public async Task CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() + { + const int workerCount = 8; + using var startGate = new ManualResetEventSlim(false); + using var allowResolutionToComplete = new ManualResetEventSlim(false); + using var workersReady = new CountdownEvent(workerCount); + var resolutionCallCount = 0; + var runtime = new Mock(MockBehavior.Strict); + var container = new Mock(MockBehavior.Strict); + + runtime.Setup(mockRuntime => mockRuntime.CreateStream( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(static () => CreateTestCqrsStream()); + + container.Setup(mockContainer => mockContainer.Get()) + .Returns(() => + { + Interlocked.Increment(ref resolutionCallCount); + allowResolutionToComplete.Wait(); + return runtime.Object; + }); + + var context = new ArchitectureContext(container.Object); + var streamTasks = Enumerable.Range(0, workerCount) + .Select(_ => Task.Run(async () => + { + workersReady.Signal(); + startGate.Wait(); + await DrainAsync(context.CreateStream(new TestCqrsStreamRequest())).ConfigureAwait(false); + })) + .ToArray(); + + ReleaseWorkersAfterFirstResolutionAttempt( + workersReady, + startGate, + allowResolutionToComplete, + () => Volatile.Read(ref resolutionCallCount) > 0); + + await Task.WhenAll(streamTasks).ConfigureAwait(false); + + Assert.That(resolutionCallCount, Is.EqualTo(1)); + container.Verify(mockContainer => mockContainer.Get(), Times.Once); + runtime.Verify( + mockRuntime => mockRuntime.CreateStream( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(streamTasks.Length)); + } + + /// + /// 枚举完整个测试流,确保 `CreateStream` 路径真正执行到底。 + /// + /// 要消费的异步流。 + /// 表示消费完成的任务。 + private static async Task DrainAsync(IAsyncEnumerable stream) + { + ArgumentNullException.ThrowIfNull(stream); + + await foreach (var _ in stream.ConfigureAwait(false)) + { + } + } + + /// + /// 释放并发 worker,并确保在断言失败时也能放行首次 runtime 解析。 + /// + /// 用于确认 worker 已就绪的倒计时器。 + /// 用于同时放行 worker 的门闩。 + /// 用于解除首次 runtime 解析阻塞的门闩。 + /// 用于判断当前是否已观察到首次 runtime 解析尝试。 + private static void ReleaseWorkersAfterFirstResolutionAttempt( + CountdownEvent workersReady, + ManualResetEventSlim startGate, + ManualResetEventSlim allowResolutionToComplete, + Func hasObservedResolutionAttempt) + { + ArgumentNullException.ThrowIfNull(workersReady); + ArgumentNullException.ThrowIfNull(startGate); + ArgumentNullException.ThrowIfNull(allowResolutionToComplete); + ArgumentNullException.ThrowIfNull(hasObservedResolutionAttempt); + + var workerStartupTimeout = TimeSpan.FromSeconds(5); + var firstResolutionTimeout = TimeSpan.FromSeconds(5); + + Assert.That( + workersReady.Wait(workerStartupTimeout), + Is.True, + "Expected all workers to be ready before releasing start gate."); + startGate.Set(); + + try + { + Assert.That( + SpinWait.SpinUntil(hasObservedResolutionAttempt, firstResolutionTimeout), + Is.True, + "Expected at least one CQRS runtime resolution attempt."); + } + finally + { + allowResolutionToComplete.Set(); + } + } + + /// + /// 为 `CreateStream` 并发解析测试提供最小异步流。 + /// + /// 只包含单个元素的异步流。 + private static async IAsyncEnumerable CreateTestCqrsStream() + { + yield return 42; + await Task.CompletedTask.ConfigureAwait(false); + } + private sealed class TestCqrsRequest : IRequest { } + + private sealed record TestCqrsNotification : INotification; + + private sealed record TestCqrsStreamRequest : IStreamRequest; } diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 13fb782e..b9a96094 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -4,6 +4,7 @@ using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; @@ -171,6 +172,30 @@ public class MicrosoftDiContainerTests Assert.That(_container.Get(), Is.SameAs(_container.Get())); } + /// + /// 测试当容器里仅预注册正式 CQRS runtime seam 时,基础设施接线会补齐 legacy alias, + /// 并保持新旧服务类型解析到同一实例。 + /// + [Test] + public void RegisterInfrastructure_Should_Backfill_Legacy_Cqrs_Runtime_Alias_With_The_Same_Instance() + { + _container.Clear(); + + var runtime = CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher")); + _container.Register(runtime); + + Assert.That(_container.Get(), Is.Null); + + CqrsTestRuntime.RegisterInfrastructure(_container); + + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.Get(), Is.SameAs(runtime)); + Assert.That(_container.Get(), Is.SameAs(runtime)); + } + /// /// 测试当没有实例时获取应返回 null 的功能 /// diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 1da8f684..0bc0c25e 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -3,6 +3,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Notification; using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Core.Services.Modules; @@ -31,6 +32,8 @@ public sealed class CqrsRuntimeModule : IServiceModule /// /// 注册默认 CQRS runtime seam 实现。 + /// 该入口会同时补齐旧命名空间下的 ICqrsRuntime 兼容别名, + /// 并保证新旧服务类型都解析到同一个 runtime 实例。 /// /// 目标依赖注入容器。 public void Register(IIocContainer container) @@ -40,16 +43,31 @@ public sealed class CqrsRuntimeModule : IServiceModule var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService"); - var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger); + var notificationPublisher = container.Get(); + var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger, notificationPublisher); var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); container.Register(runtime); - container.Register((LegacyICqrsRuntime)runtime); + RegisterLegacyRuntimeAlias(container, runtime); container.Register(registrar); container.Register( CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger)); } + /// + /// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。 + /// + /// 承载运行时实例的依赖注入容器。 + /// 当前已创建的新 CQRS runtime 实例。 + /// + /// 旧接口仍作为兼容入口保留,因此这里明确把别名注册收敛到单独 helper, + /// 便于后续独立评估 alias 收口,而不混入 runtime 主体行为。 + /// + private static void RegisterLegacyRuntimeAlias(IIocContainer container, ICqrsRuntime runtime) + { + container.Register((LegacyICqrsRuntime)runtime); + } + /// /// 初始化模块。 /// diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index ccd7e8cb..beb22f1e 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -5,11 +5,16 @@ namespace GFramework.Cqrs.SourceGenerators.Cqrs; /// public sealed partial class CqrsHandlerRegistryGenerator { + private readonly record struct RequestInvokerRegistrationSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName); + private readonly record struct HandlerRegistrationSpec( string HandlerInterfaceDisplayName, string ImplementationTypeDisplayName, string HandlerInterfaceLogName, - string ImplementationLogName); + string ImplementationLogName, + RequestInvokerRegistrationSpec? RequestInvokerRegistration); private readonly record struct ReflectedImplementationRegistrationSpec( string HandlerInterfaceDisplayName, @@ -24,14 +29,24 @@ public sealed partial class CqrsHandlerRegistryGenerator bool HasReflectedImplementationRegistrations, bool HasPreciseReflectedRegistrations, bool HasReflectionTypeLookups, - bool HasExternalAssemblyTypeLookups) + bool HasExternalAssemblyTypeLookups, + bool SupportsRequestInvokerProvider, + ImmutableArray RequestInvokerEmissions) { public bool RequiresRegistryAssemblyVariable => HasReflectedImplementationRegistrations || HasPreciseReflectedRegistrations || HasReflectionTypeLookups; + + public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty; } + private readonly record struct RequestInvokerEmissionSpec( + string RequestTypeDisplayName, + string ResponseTypeDisplayName, + string HandlerInterfaceDisplayName, + int MethodIndex); + /// /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 /// @@ -312,5 +327,6 @@ public sealed partial class CqrsHandlerRegistryGenerator bool GenerationEnabled, bool SupportsNamedReflectionFallbackTypes, bool SupportsDirectReflectionFallbackTypes, - bool SupportsMultipleReflectionFallbackAttributes); + bool SupportsMultipleReflectionFallbackAttributes, + bool SupportsRequestInvokerProvider); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index 9dd72b7f..858c00d9 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -25,10 +25,11 @@ public sealed partial class CqrsHandlerRegistryGenerator /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 /// private static string GenerateSource( + GenerationEnvironment generationEnvironment, IReadOnlyList registrations, ReflectionFallbackEmissionSpec reflectionFallbackEmission) { - var sourceShape = CreateGeneratedRegistrySourceShape(registrations); + var sourceShape = CreateGeneratedRegistrySourceShape(generationEnvironment, registrations); var builder = new StringBuilder(); AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission); AppendGeneratedRegistryType(builder, registrations, sourceShape); @@ -41,6 +42,7 @@ public sealed partial class CqrsHandlerRegistryGenerator /// 已整理并排序的 handler 注册描述。 /// 当前生成输出需要启用的结构分支。 private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape( + GenerationEnvironment generationEnvironment, IReadOnlyList registrations) { var hasReflectedImplementationRegistrations = registrations.Any(static registration => @@ -52,12 +54,61 @@ public sealed partial class CqrsHandlerRegistryGenerator var hasExternalAssemblyTypeLookups = registrations.Any(static registration => registration.PreciseReflectedRegistrations.Any(static preciseRegistration => preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); + var requestInvokerEmissions = CreateRequestInvokerEmissions( + generationEnvironment.SupportsRequestInvokerProvider, + registrations); return new GeneratedRegistrySourceShape( hasReflectedImplementationRegistrations, hasPreciseReflectedRegistrations, hasReflectionTypeLookups, - hasExternalAssemblyTypeLookups); + hasExternalAssemblyTypeLookups, + generationEnvironment.SupportsRequestInvokerProvider, + requestInvokerEmissions); + } + + /// + /// 从 direct handler 注册描述中提取 request invoker 发射计划。 + /// + /// + /// 指示当前 runtime 是否同时暴露 ICqrsRequestInvokerProvider 与 + /// IEnumeratesCqrsRequestInvokerDescriptors 契约;若不支持,则本方法必须返回空结果并让后续发射路径整体跳过。 + /// + /// 已按稳定顺序整理完成的 handler 注册描述。 + /// + /// 由 directRegistration.RequestInvokerRegistration 派生出的 集合。 + /// methodIndex 与其 direct registration 的遍历顺序单调递增, + /// 因而只要上游排序稳定,生成的 invoker 方法名与描述符顺序就跨运行保持稳定。 + /// + /// + /// 缺少 RequestInvokerRegistration 的 direct registration 会被显式跳过,而不会生成半成品 provider 成员; + /// 调用方应把“为什么没有 request invoker registration”对应的诊断留在更早的建模阶段,而不是在源码发射阶段兜底。 + /// + private static ImmutableArray CreateRequestInvokerEmissions( + bool supportsRequestInvokerProvider, + IReadOnlyList registrations) + { + if (!supportsRequestInvokerProvider) + return ImmutableArray.Empty; + + var builder = ImmutableArray.CreateBuilder(); + var methodIndex = 0; + foreach (var registration in registrations) + { + foreach (var directRegistration in registration.DirectRegistrations) + { + if (directRegistration.RequestInvokerRegistration is not { } requestInvokerRegistration) + continue; + + builder.Add(new RequestInvokerEmissionSpec( + requestInvokerRegistration.RequestTypeDisplayName, + requestInvokerRegistration.ResponseTypeDisplayName, + directRegistration.HandlerInterfaceDisplayName, + methodIndex++)); + } + } + + return builder.ToImmutable(); } /// @@ -160,10 +211,26 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.Append(GeneratedTypeName); builder.Append(" : global::"); builder.Append(CqrsRuntimeNamespace); - builder.AppendLine(".ICqrsHandlerRegistry"); + builder.Append(".ICqrsHandlerRegistry"); + if (sourceShape.HasRequestInvokerProvider) + { + builder.Append(", global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".ICqrsRequestInvokerProvider, global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors"); + } + + builder.AppendLine(); builder.AppendLine("{"); AppendRegisterMethod(builder, registrations, sourceShape); + if (sourceShape.HasRequestInvokerProvider) + { + builder.AppendLine(); + AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions); + } + if (sourceShape.HasExternalAssemblyTypeLookups) { builder.AppendLine(); @@ -223,6 +290,140 @@ public sealed partial class CqrsHandlerRegistryGenerator builder.AppendLine(" }"); } + /// + /// 发射 generated registry 的 request invoker provider 成员。 + /// + /// 生成源码构造器。 + /// + /// 来自 的稳定发射计划。 + /// + /// + /// 该输出包含三部分:描述符数组、provider 查询方法,以及与描述符逐项对应的静态 invoker 方法。 + /// 若发射计划为空,调用方应直接跳过整个 provider 分支,而不是输出空的 registry seam。 + /// + private static void AppendRequestInvokerProviderMembers( + StringBuilder builder, + ImmutableArray requestInvokerEmissions) + { + AppendRequestInvokerDescriptorArray(builder, requestInvokerEmissions); + builder.AppendLine(); + AppendRequestInvokerProviderMethods(builder); + + for (var index = 0; index < requestInvokerEmissions.Length; index++) + { + builder.AppendLine(); + AppendRequestInvokerMethod(builder, requestInvokerEmissions[index]); + } + } + + /// + /// 发射 generated registry 的 request invoker 描述符数组。 + /// + /// 生成源码构造器。 + /// 当前要输出的 request invoker 发射计划。 + /// + /// 每个条目都会把请求类型、响应类型和对应的静态 invoker 方法打包成 + /// CqrsRequestInvokerDescriptorEntry,供 registrar 在注册阶段写入 dispatcher 的弱缓存。 + /// + private static void AppendRequestInvokerDescriptorArray( + StringBuilder builder, + ImmutableArray requestInvokerEmissions) + { + builder.AppendLine(" private static readonly global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry[] RequestInvokerDescriptors ="); + builder.AppendLine(" ["); + + for (var index = 0; index < requestInvokerEmissions.Length; index++) + { + var emission = requestInvokerEmissions[index]; + builder.Append(" new global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsRequestInvokerDescriptorEntry(typeof("); + builder.Append(emission.RequestTypeDisplayName); + builder.Append("), typeof("); + builder.Append(emission.ResponseTypeDisplayName); + builder.Append("), new global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsRequestInvokerDescriptor(typeof("); + builder.Append(emission.HandlerInterfaceDisplayName); + builder.Append("), typeof("); + builder.Append(GeneratedTypeName); + builder.Append(").GetMethod(nameof(InvokeRequestHandler"); + builder.Append(emission.MethodIndex); + builder.Append("), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))"); + builder.AppendLine(index == requestInvokerEmissions.Length - 1 ? string.Empty : ","); + } + + builder.AppendLine(" ];"); + } + + /// + /// 发射 generated registry 对 request invoker provider 契约的实现方法。 + /// + /// 生成源码构造器。 + /// + /// 默认 runtime 真正消费的是 GetDescriptors() 暴露的完整描述符集合,并在注册阶段一次性写入缓存; + /// TryGetDescriptor(...) 保留为显式查询接口,因此这里使用线性扫描即可保持生成代码简单且无额外字典分配。 + /// + private static void AppendRequestInvokerProviderMethods(StringBuilder builder) + { + builder.Append(" public global::System.Collections.Generic.IReadOnlyList GetDescriptors()"); + builder.AppendLine(" {"); + builder.AppendLine(" return RequestInvokerDescriptors;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.Append(" public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".CqrsRequestInvokerDescriptor? descriptor)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (requestType is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(requestType));"); + builder.AppendLine(" if (responseType is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(responseType));"); + builder.AppendLine(); + builder.AppendLine(" foreach (var entry in RequestInvokerDescriptors)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (entry.RequestType == requestType && entry.ResponseType == responseType)"); + builder.AppendLine(" {"); + builder.AppendLine(" descriptor = entry.Descriptor;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" descriptor = null;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + } + + /// + /// 为单个 request invoker 描述符发射对应的静态强类型桥接方法。 + /// + /// 生成源码构造器。 + /// 当前要输出的 invoker 发射计划。 + /// + /// 这些方法的编号与 一一对应, + /// dispatcher 通过描述符里的 把 object 形参桥接回强类型 handler 与 request。 + /// + private static void AppendRequestInvokerMethod(StringBuilder builder, RequestInvokerEmissionSpec emission) + { + builder.Append(" private static global::System.Threading.Tasks.ValueTask<"); + builder.Append(emission.ResponseTypeDisplayName); + builder.Append("> InvokeRequestHandler"); + builder.Append(emission.MethodIndex); + builder.Append("(object handler, object request, global::System.Threading.CancellationToken cancellationToken)"); + builder.AppendLine(); + builder.AppendLine(" {"); + builder.Append(" var typedHandler = ("); + builder.Append(emission.HandlerInterfaceDisplayName); + builder.AppendLine(")handler;"); + builder.Append(" var typedRequest = ("); + builder.Append(emission.RequestTypeDisplayName); + builder.AppendLine(")request;"); + builder.AppendLine(" return typedHandler.Handle(typedRequest, cancellationToken);"); + builder.AppendLine(" }"); + } + private static void AppendDirectRegistrations( StringBuilder builder, ImplementationRegistrationSpec registration) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 5ebe31b0..db5f1bc7 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -15,6 +15,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1"; private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; + private const string ICqrsRequestInvokerProviderMetadataName = $"{CqrsRuntimeNamespace}.ICqrsRequestInvokerProvider"; + private const string IEnumeratesCqrsRequestInvokerDescriptorsMetadataName = + $"{CqrsRuntimeNamespace}.IEnumeratesCqrsRequestInvokerDescriptors"; + private const string CqrsRequestInvokerDescriptorMetadataName = + $"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor"; + private const string CqrsRequestInvokerDescriptorEntryMetadataName = + $"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry"; private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; @@ -66,6 +73,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator CqrsHandlerRegistryAttributeMetadataName) is not null && compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; + var supportsRequestInvokerProvider = + compilation.GetTypeByMetadataName(ICqrsRequestInvokerProviderMetadataName) is not null && + compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) is not null; var stringType = compilation.GetSpecialType(SpecialType.System_String); var typeType = compilation.GetTypeByMetadataName("System.Type"); var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null && @@ -85,7 +97,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator generationEnabled, supportsNamedReflectionFallbackTypes, supportsDirectReflectionFallbackTypes, - supportsMultipleReflectionFallbackAttributes); + supportsMultipleReflectionFallbackAttributes, + supportsRequestInvokerProvider); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -218,7 +231,10 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), implementationTypeDisplayName, GetLogDisplayName(handlerInterface), - implementationLogName)); + implementationLogName, + TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration) + ? requestInvokerRegistration + : null)); return true; } @@ -237,6 +253,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator return true; } + /// + /// 当当前直接注册项属于请求处理器时,提取 request invoker provider 所需的请求/响应类型显示名。 + /// + private static bool TryCreateRequestInvokerRegistrationSpec( + INamedTypeSymbol handlerInterface, + out RequestInvokerRegistrationSpec requestInvokerRegistration) + { + if (!string.Equals( + handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + $"global::{CqrsContractsNamespace}.IRequestHandler", + StringComparison.Ordinal)) + { + requestInvokerRegistration = default; + return false; + } + + if (handlerInterface.TypeArguments.Length != 2) + { + requestInvokerRegistration = default; + return false; + } + + requestInvokerRegistration = new RequestInvokerRegistrationSpec( + handlerInterface.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + handlerInterface.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + /// /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 /// CqrsHandlerRegistry.g.cs,并在需要时附带程序集级 reflection fallback 元数据。 @@ -296,7 +340,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator context.AddSource( HintName, - GenerateSource(registrations, reflectionFallbackEmission)); + GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission)); } /// diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Cqrs/ArchitectureContextComprehensiveTests.cs similarity index 61% rename from GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs rename to GFramework.Cqrs.Tests/Cqrs/ArchitectureContextComprehensiveTests.cs index 0871144b..be08d405 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/ArchitectureContextComprehensiveTests.cs @@ -11,10 +11,13 @@ using GFramework.Core.Query; using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; -namespace GFramework.Cqrs.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Cqrs; +/// +/// 覆盖 在 CQRS 请求、通知、流式处理和传统总线共存场景下的综合行为。 +/// [TestFixture] -public class MediatorComprehensiveTests +public sealed class ArchitectureContextComprehensiveTests { /// /// 测试初始化方法,在每个测试方法执行前运行。 @@ -29,9 +32,9 @@ public class MediatorComprehensiveTests var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance); loggerField?.SetValue(_container, - LoggerFactoryResolver.Provider.CreateLogger(nameof(MediatorComprehensiveTests))); + LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContextComprehensiveTests))); - // 注册基础服务(Legacy CQRS) + // 注册传统 CQRS 基础服务,验证其与 ArchitectureContext 请求管线可以共存。 _eventBus = new EventBus(); _commandBus = new CommandExecutor(); _queryBus = new QueryExecutor(); @@ -46,7 +49,7 @@ public class MediatorComprehensiveTests CqrsTestRuntime.RegisterHandlers( _container, - typeof(MediatorComprehensiveTests).Assembly, + typeof(ArchitectureContextComprehensiveTests).Assembly, typeof(ArchitectureContext).Assembly); _container.Freeze(); @@ -416,306 +419,304 @@ public class MediatorComprehensiveTests Assert.That(legacyCommand.Executed, Is.True); // 使用自有 CQRS 方式 - var mediatorCommand = new TestCommandWithResult { ResultValue = 999 }; - var result = await _context.SendAsync(mediatorCommand).ConfigureAwait(false); + var cqrsCommand = new TestCommandWithResult { ResultValue = 999 }; + var result = await _context.SendAsync(cqrsCommand).ConfigureAwait(false); Assert.That(result, Is.EqualTo(999)); // 验证两者可以同时工作 Assert.That(legacyCommand.Executed, Is.True); Assert.That(result, Is.EqualTo(999)); } -#region Advanced Test Classes for CQRS Features + #region ArchitectureContext CQRS Test Helpers -public sealed record TestLongRunningRequest : IRequest -{ - public int DelayMs { get; init; } -} - -public sealed class TestLongRunningRequestHandler : IRequestHandler -{ - public async ValueTask Handle(TestLongRunningRequest request, CancellationToken cancellationToken) + private sealed record TestLongRunningRequest : IRequest { - await Task.Delay(request.DelayMs, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - return "Completed"; + public int DelayMs { get; init; } } -} -public sealed record TestLongStreamRequest : IStreamRequest -{ - public int ItemCount { get; init; } -} - -public sealed class TestLongStreamRequestHandler : IStreamRequestHandler -{ - public async IAsyncEnumerable Handle( - TestLongStreamRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken) + private sealed class TestLongRunningRequestHandler : IRequestHandler { - for (int i = 0; i < request.ItemCount; i++) + public async ValueTask Handle(TestLongRunningRequest request, CancellationToken cancellationToken) { + await Task.Delay(request.DelayMs, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - yield return i; - await Task.Delay(10, cancellationToken).ConfigureAwait(false); // 模拟处理延迟 + return "Completed"; } } -} -public sealed record TestFaultyRequest : IRequest; - -public sealed class TestFaultyRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestFaultyRequest request, CancellationToken cancellationToken) + private sealed record TestLongStreamRequest : IStreamRequest { - throw new InvalidOperationException("Handler failed intentionally"); + public int ItemCount { get; init; } } -} -public class SharedData -{ - public int Value { get; set; } -} - -public sealed record TestModifyDataCommand : IRequest -{ - public SharedData Data { get; init; } = null!; - public int Value { get; init; } -} - -public sealed class TestModifyDataCommandHandler : IRequestHandler -{ - public ValueTask Handle(TestModifyDataCommand request, CancellationToken cancellationToken) + private sealed class TestLongStreamRequestHandler : IStreamRequestHandler { - request.Data.Value += request.Value; - return ValueTask.FromResult(Unit.Value); - } -} - -public sealed record TestCachingQuery : IRequest -{ - public string Key { get; init; } = string.Empty; - public IDictionary Cache { get; init; } = new Dictionary(StringComparer.Ordinal); -} - -public sealed class TestCachingQueryHandler : IRequestHandler -{ - public ValueTask Handle(TestCachingQuery request, CancellationToken cancellationToken) - { - if (request.Cache.TryGetValue(request.Key, out var cachedValue)) + public async IAsyncEnumerable Handle( + TestLongStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) { - return new ValueTask(cachedValue); + for (int i = 0; i < request.ItemCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return i; + await Task.Delay(10, cancellationToken).ConfigureAwait(false); // 模拟处理延迟 + } } - - var newValue = $"Value_for_{request.Key}"; - request.Cache[request.Key] = newValue; - return new ValueTask(newValue); } -} -public sealed record TestOrderedNotification : INotification -{ - public int Order { get; init; } - public string Message { get; init; } = string.Empty; -} + private sealed record TestFaultyRequest : IRequest; -public sealed class TestOrderedNotificationHandler : INotificationHandler -{ - public static ICollection ReceivedMessages { get; set; } = new List(); - - public ValueTask Handle(TestOrderedNotification notification, CancellationToken cancellationToken) + private sealed class TestFaultyRequestHandler : IRequestHandler { - ReceivedMessages.Add(notification.Message); - return ValueTask.CompletedTask; - } -} - -// 额外的通知处理器来测试多处理器场景 -public sealed class TestNotificationHandler2 : INotificationHandler -{ - public static string? LastReceivedMessage { get; set; } - - public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) - { - LastReceivedMessage = notification.Message; - return ValueTask.CompletedTask; - } -} - -public sealed class TestNotificationHandler3 : INotificationHandler -{ - public static string? LastReceivedMessage { get; set; } - - public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) - { - LastReceivedMessage = notification.Message; - return ValueTask.CompletedTask; - } -} - -public sealed record TestFilterStreamRequest : IStreamRequest -{ - public int[] Values { get; init; } = []; - public bool FilterEven { get; init; } -} - -public sealed class TestFilterStreamRequestHandler : IStreamRequestHandler -{ - public async IAsyncEnumerable Handle( - TestFilterStreamRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - foreach (var value in request.Values) + public ValueTask Handle(TestFaultyRequest request, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - if (request.FilterEven && value % 2 != 0) - continue; - - yield return value; - await Task.Yield(); + throw new InvalidOperationException("Handler failed intentionally"); } } -} -public sealed record TestValidatedCommand : IRequest -{ - public string Name { get; init; } = string.Empty; -} - -public sealed class TestValidatedCommandHandler : IRequestHandler -{ - public ValueTask Handle(TestValidatedCommand request, CancellationToken cancellationToken) + private sealed class SharedData { - if (string.IsNullOrWhiteSpace(request.Name)) + public int Value { get; set; } + } + + private sealed record TestModifyDataCommand : IRequest + { + public SharedData Data { get; init; } = null!; + public int Value { get; init; } + } + + private sealed class TestModifyDataCommandHandler : IRequestHandler + { + public ValueTask Handle(TestModifyDataCommand request, CancellationToken cancellationToken) { - throw new ArgumentException("Name cannot be empty.", nameof(request)); + request.Data.Value += request.Value; + return ValueTask.FromResult(Unit.Value); } - - return ValueTask.FromResult(Unit.Value); - } -} - -// 传统命令用于共存测试 -public class TestLegacyCommand : ICommand -{ - public bool Executed { get; private set; } - - public void Execute() - { - Executed = true; } - public void SetContext(IArchitectureContext context) + private sealed record TestCachingQuery : IRequest { - // 不需要实现 + public string Key { get; init; } = string.Empty; + public IDictionary Cache { get; init; } = new Dictionary(StringComparer.Ordinal); } - public IArchitectureContext GetContext() + private sealed class TestCachingQueryHandler : IRequestHandler { - return null!; - } -} - -#endregion - -#region Test Classes - CQRS Runtime - -// ✅ 这些类使用自有 CQRS IRequest -public sealed record TestRequest : IRequest -{ - public int Value { get; init; } -} - -public sealed record TestCommand : IRequest -{ - public bool ShouldExecute { get; init; } - public bool Executed { get; set; } -} - -public sealed record TestCommandWithResult : IRequest -{ - public int ResultValue { get; init; } -} - -public sealed record TestQuery : IRequest -{ - public string QueryResult { get; init; } = string.Empty; -} - -public sealed record TestNotification : INotification -{ - public string Message { get; init; } = string.Empty; -} - -public sealed record TestStreamRequest : IStreamRequest -{ - public int[] Values { get; init; } = []; -} - -// ✅ 这些 Handler 使用自有 CQRS IRequestHandler -public sealed class TestRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestRequest request, CancellationToken cancellationToken) - { - return new ValueTask(request.Value); - } -} - -public sealed class TestCommandHandler : IRequestHandler -{ - public ValueTask Handle(TestCommand request, CancellationToken cancellationToken) - { - if (request.ShouldExecute) + public ValueTask Handle(TestCachingQuery request, CancellationToken cancellationToken) { - request.Executed = true; + if (request.Cache.TryGetValue(request.Key, out var cachedValue)) + { + return new ValueTask(cachedValue); + } + + var newValue = $"Value_for_{request.Key}"; + request.Cache[request.Key] = newValue; + return new ValueTask(newValue); } - - return ValueTask.FromResult(Unit.Value); } -} -public sealed class TestCommandWithResultHandler : IRequestHandler -{ - public ValueTask Handle(TestCommandWithResult request, CancellationToken cancellationToken) + private sealed record TestOrderedNotification : INotification { - return new ValueTask(request.ResultValue); + public int Order { get; init; } + public string Message { get; init; } = string.Empty; } -} -public sealed class TestQueryHandler : IRequestHandler -{ - public ValueTask Handle(TestQuery request, CancellationToken cancellationToken) + private sealed class TestOrderedNotificationHandler : INotificationHandler { - return new ValueTask(request.QueryResult); - } -} + public static ICollection ReceivedMessages { get; set; } = new List(); -public sealed class TestNotificationHandler : INotificationHandler -{ - public static string? LastReceivedMessage { get; set; } - - public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) - { - LastReceivedMessage = notification.Message; - return ValueTask.CompletedTask; - } -} - -public sealed class TestStreamRequestHandler : IStreamRequestHandler -{ - public async IAsyncEnumerable Handle( - TestStreamRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - foreach (var value in request.Values) + public ValueTask Handle(TestOrderedNotification notification, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - yield return value; - await Task.Yield(); + ReceivedMessages.Add(notification.Message); + return ValueTask.CompletedTask; } } -} -#endregion + // 额外的通知处理器用于验证多处理器通知分发场景。 + private sealed class TestNotificationHandler2 : INotificationHandler + { + public static string? LastReceivedMessage { get; set; } + + public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + LastReceivedMessage = notification.Message; + return ValueTask.CompletedTask; + } + } + + private sealed class TestNotificationHandler3 : INotificationHandler + { + public static string? LastReceivedMessage { get; set; } + + public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + LastReceivedMessage = notification.Message; + return ValueTask.CompletedTask; + } + } + + private sealed record TestFilterStreamRequest : IStreamRequest + { + public int[] Values { get; init; } = []; + public bool FilterEven { get; init; } + } + + private sealed class TestFilterStreamRequestHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle( + TestFilterStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var value in request.Values) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (request.FilterEven && value % 2 != 0) + continue; + + yield return value; + await Task.Yield(); + } + } + } + + private sealed record TestValidatedCommand : IRequest + { + public string Name { get; init; } = string.Empty; + } + + private sealed class TestValidatedCommandHandler : IRequestHandler + { + public ValueTask Handle(TestValidatedCommand request, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentException("Name cannot be empty.", nameof(request)); + } + + return ValueTask.FromResult(Unit.Value); + } + } + + // 传统命令用于验证旧命令总线与 ArchitectureContext 的 CQRS API 可以同时工作。 + private sealed class TestLegacyCommand : ICommand + { + public bool Executed { get; private set; } + + public void Execute() + { + Executed = true; + } + + public void SetContext(IArchitectureContext context) + { + // 该测试只验证传统命令是否被执行,不依赖上下文回填。 + } + + public IArchitectureContext GetContext() + { + return null!; + } + } + + #endregion + + #region ArchitectureContext CQRS Runtime Types + + private sealed record TestRequest : IRequest + { + public int Value { get; init; } + } + + private sealed record TestCommand : IRequest + { + public bool ShouldExecute { get; init; } + public bool Executed { get; set; } + } + + private sealed record TestCommandWithResult : IRequest + { + public int ResultValue { get; init; } + } + + private sealed record TestQuery : IRequest + { + public string QueryResult { get; init; } = string.Empty; + } + + private sealed record TestNotification : INotification + { + public string Message { get; init; } = string.Empty; + } + + private sealed record TestStreamRequest : IStreamRequest + { + public int[] Values { get; init; } = []; + } + + private sealed class TestRequestHandler : IRequestHandler + { + public ValueTask Handle(TestRequest request, CancellationToken cancellationToken) + { + return new ValueTask(request.Value); + } + } + + private sealed class TestCommandHandler : IRequestHandler + { + public ValueTask Handle(TestCommand request, CancellationToken cancellationToken) + { + if (request.ShouldExecute) + { + request.Executed = true; + } + + return ValueTask.FromResult(Unit.Value); + } + } + + private sealed class TestCommandWithResultHandler : IRequestHandler + { + public ValueTask Handle(TestCommandWithResult request, CancellationToken cancellationToken) + { + return new ValueTask(request.ResultValue); + } + } + + private sealed class TestQueryHandler : IRequestHandler + { + public ValueTask Handle(TestQuery request, CancellationToken cancellationToken) + { + return new ValueTask(request.QueryResult); + } + } + + private sealed class TestNotificationHandler : INotificationHandler + { + public static string? LastReceivedMessage { get; set; } + + public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + LastReceivedMessage = notification.Message; + return ValueTask.CompletedTask; + } + } + + private sealed class TestStreamRequestHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle( + TestStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var value in request.Values) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return value; + await Task.Yield(); + } + } + } + + #endregion } diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs similarity index 68% rename from GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs rename to GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs index d8e9de59..63fb487b 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextAdvancedFeaturesTests.cs @@ -4,15 +4,20 @@ using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Cqrs.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Cqrs; /// -/// Mediator高级特性专项测试 -/// 专注于测试Mediator框架的高级功能和边界场景 +/// 验证 CQRS 请求通过 分发时的高级行为与边界场景。 /// [TestFixture] -public class MediatorAdvancedFeaturesTests +internal sealed class CqrsArchitectureContextAdvancedFeaturesTests { + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + + /// + /// 初始化测试容器、日志器和 CQRS 处理器注册表。 + /// [SetUp] public void SetUp() { @@ -23,17 +28,20 @@ public class MediatorAdvancedFeaturesTests var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance); loggerField?.SetValue(_container, - LoggerFactoryResolver.Provider.CreateLogger(nameof(MediatorAdvancedFeaturesTests))); + LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsArchitectureContextAdvancedFeaturesTests))); CqrsTestRuntime.RegisterHandlers( _container, - typeof(MediatorAdvancedFeaturesTests).Assembly, + typeof(CqrsArchitectureContextAdvancedFeaturesTests).Assembly, typeof(ArchitectureContext).Assembly); _container.Freeze(); _context = new ArchitectureContext(_container); } + /// + /// 释放当前测试用到的上下文和容器引用。 + /// [TearDown] public void TearDown() { @@ -41,11 +49,9 @@ public class MediatorAdvancedFeaturesTests _container = null; } - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - - + /// + /// 验证请求验证逻辑会阻止无效输入继续进入 CQRS 处理流程。 + /// [Test] public async Task Request_With_Validation_Behavior_Should_Validate_Input() { @@ -56,7 +62,7 @@ public class MediatorAdvancedFeaturesTests } [Test] - public async Task Request_With_Retry_Behavior_Should_Retry_On_Failure() + public async Task Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt() { // 由于我们没有实现实际的重试行为,简化测试逻辑 TestRetryBehavior.AttemptCount = 0; @@ -68,8 +74,11 @@ public class MediatorAdvancedFeaturesTests Assert.That(TestRetryBehavior.AttemptCount, Is.EqualTo(1)); } + /// + /// 验证高并发 CQRS 请求可以在合理时间内全部完成。 + /// [Test] - public async Task High_Concurrency_Mediator_Requests_Should_Handle_Efficiently() + public async Task High_Concurrency_Cqrs_Requests_Should_Handle_Efficiently() { const int concurrentRequests = 100; var tasks = new List>(); @@ -93,6 +102,9 @@ public class MediatorAdvancedFeaturesTests Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(5000)); // 5秒内完成 } + /// + /// 验证大批量请求下的内存占用不会出现明显泄漏。 + /// [Test] public async Task Memory_Usage_Should_Remain_Stable_Under_Heavy_Load() { @@ -120,7 +132,7 @@ public class MediatorAdvancedFeaturesTests } [Test] - public async Task Transient_Error_Should_Be_Handled_By_Retry_Mechanism() + public async Task Transient_Error_Request_Should_Succeed_Without_Simulated_Errors() { // 由于我们没有实现实际的瞬态错误处理,简化测试逻辑 TestTransientErrorHandler.ErrorCount = 0; @@ -132,6 +144,9 @@ public class MediatorAdvancedFeaturesTests Assert.That(TestTransientErrorHandler.ErrorCount, Is.EqualTo(0)); } + /// + /// 验证断路器在持续失败后会快速拒绝后续请求。 + /// [Test] public async Task Circuit_Breaker_Should_Prevent_Cascading_Failures() { @@ -160,6 +175,9 @@ public class MediatorAdvancedFeaturesTests Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(100)); } + /// + /// 验证多步 Saga 请求在全部成功时会保持一致的完成状态。 + /// [Test] public async Task Saga_Pattern_With_Multiple_Requests_Should_Maintain_Consistency() { @@ -182,6 +200,9 @@ public class MediatorAdvancedFeaturesTests Assert.That(sagaData.IsCompleted, Is.True); } + /// + /// 验证 Saga 在中途失败时会触发既有步骤的补偿逻辑。 + /// [Test] public async Task Saga_With_Failure_Should_Rollback_Correctly() { @@ -205,6 +226,9 @@ public class MediatorAdvancedFeaturesTests Assert.That(sagaData.IsCompleted, Is.False); } + /// + /// 验证请求链可以在同一架构上下文中顺序完成。 + /// [Test] public async Task Request_Chaining_With_Dependencies_Should_Work_Correctly() { @@ -213,8 +237,11 @@ public class MediatorAdvancedFeaturesTests Assert.That(chainResult, Is.EqualTo("Chain completed: Step1 -> Step2 -> Step3")); } + /// + /// 验证 CQRS 请求依赖外部服务时会正确传播取消超时。 + /// [Test] - public async Task Mediator_With_External_Service_Dependency_Should_Handle_Timeouts() + public async Task Cqrs_With_External_Service_Dependency_Should_Handle_Timeouts() { using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); var request = new TestExternalServiceRequest { TimeoutMs = 1000 }; @@ -223,8 +250,11 @@ public class MediatorAdvancedFeaturesTests await _context!.SendRequestAsync(request, cts.Token).ConfigureAwait(false)); } + /// + /// 验证 CQRS 请求封装数据库写入时仍能保持事务语义上的可观察结果。 + /// [Test] - public async Task Mediator_With_Database_Operations_Should_Handle_Transactions() + public async Task Cqrs_With_Database_Operations_Should_Handle_Transactions() { var testData = new List(); var request = new TestDatabaseRequest { Data = "test data", Storage = testData }; @@ -236,12 +266,16 @@ public class MediatorAdvancedFeaturesTests } } -// 这些高级特性测试需要把一组仅供当前文件使用的辅助类型共置,避免拆成多个噪声文件。 +// 这些 CQRS/ArchitectureContext 高级场景测试需要把一组仅供当前文件使用的辅助类型共置,避免拆成多个噪声文件。 #pragma warning disable MA0048 #region Advanced Test Classes +/// +/// 处理重试请求,并在达到成功条件前累积尝试次数。 +/// public sealed class TestRetryRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestRetryRequest request, CancellationToken cancellationToken) { TestRetryBehavior.AttemptCount++; @@ -255,8 +289,12 @@ public sealed class TestRetryRequestHandler : IRequestHandler +/// 处理瞬态错误请求,并在配置次数内模拟失败。 +/// public sealed class TestTransientErrorRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestTransientErrorRequest request, CancellationToken cancellationToken) { // 只有在MaxErrors > 0时才增加计数器 @@ -274,8 +312,12 @@ public sealed class TestTransientErrorRequestHandler : IRequestHandler +/// 处理断路器请求,并根据失败阈值切换断路器状态。 +/// public sealed class TestCircuitBreakerRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestCircuitBreakerRequest request, CancellationToken cancellationToken) { // 检查断路器状态 @@ -302,8 +344,12 @@ public sealed class TestCircuitBreakerRequestHandler : IRequestHandler +/// 处理 Saga 步骤请求,并在失败时记录补偿步骤。 +/// public sealed class TestSagaStepRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestSagaStepRequest request, CancellationToken cancellationToken) { if (request.ShouldFail && request.Step == 2) @@ -328,8 +374,12 @@ public sealed class TestSagaStepRequestHandler : IRequestHandler +/// 处理链式请求,并返回预定义的链路完成结果。 +/// public sealed class TestChainStartRequestHandler : IRequestHandler { + /// public async ValueTask Handle(TestChainStartRequest request, CancellationToken cancellationToken) { // 模拟链式调用 @@ -338,8 +388,12 @@ public sealed class TestChainStartRequestHandler : IRequestHandler +/// 处理外部服务请求,并通过延时模拟超时场景。 +/// public sealed class TestExternalServiceRequestHandler : IRequestHandler { + /// public async ValueTask Handle(TestExternalServiceRequest request, CancellationToken cancellationToken) { await Task.Delay(request.TimeoutMs, cancellationToken).ConfigureAwait(false); @@ -348,8 +402,12 @@ public sealed class TestExternalServiceRequestHandler : IRequestHandler +/// 处理数据库请求,并把输入数据写入模拟存储集合。 +/// public sealed class TestDatabaseRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestDatabaseRequest request, CancellationToken cancellationToken) { request.Storage.Add(request.Data); @@ -357,26 +415,46 @@ public sealed class TestDatabaseRequestHandler : IRequestHandler +/// 表示用于简单行为验证的测试请求。 +/// public sealed record TestBehaviorRequest : IRequest { + /// + /// 获取或初始化要原样返回的消息内容。 + /// public string Message { get; init; } = string.Empty; } +/// +/// 处理简单行为请求,并回显请求消息。 +/// public sealed class TestBehaviorRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestBehaviorRequest request, CancellationToken cancellationToken) { return new ValueTask(request.Message); } } +/// +/// 表示带输入校验约束的测试请求。 +/// public sealed record TestValidatedRequest : IRequest { + /// + /// 获取或初始化要验证的整数值。 + /// public int Value { get; init; } } +/// +/// 处理带校验的请求,并在输入无效时抛出异常。 +/// public sealed class TestValidatedRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestValidatedRequest request, CancellationToken cancellationToken) { // 验证输入 @@ -389,25 +467,50 @@ public sealed class TestValidatedRequestHandler : IRequestHandler +/// 表示需要在若干次失败后才能成功的重试测试请求。 +/// public sealed record TestRetryRequest : IRequest { + /// + /// 获取或初始化在返回成功前应模拟的失败次数。 + /// public int ShouldFailTimes { get; init; } } +/// +/// 保存重试测试的共享计数状态。 +/// public static class TestRetryBehavior { + /// + /// 获取或设置当前请求处理期间累计的尝试次数。 + /// public static int AttemptCount { get; set; } } -// 性能测试相关类 +/// +/// 表示用于并发与性能验证的测试请求。 +/// public sealed record TestPerformanceRequest : IRequest { + /// + /// 获取或初始化请求的标识值。 + /// public int Id { get; init; } + + /// + /// 获取或初始化模拟处理延时,单位为毫秒。 + /// public int ProcessingTimeMs { get; init; } } +/// +/// 处理性能请求,并在延时后返回请求标识。 +/// public sealed class TestPerformanceRequestHandler : IRequestHandler { + /// public async ValueTask Handle(TestPerformanceRequest request, CancellationToken cancellationToken) { await Task.Delay(request.ProcessingTimeMs, cancellationToken).ConfigureAwait(false); @@ -415,13 +518,23 @@ public sealed class TestPerformanceRequestHandler : IRequestHandler +/// 表示用于内存占用验证的测试请求。 +/// public sealed record TestMemoryRequest : IRequest { + /// + /// 获取或初始化用于模拟负载的数据内容。 + /// public string Data { get; init; } = string.Empty; } +/// +/// 处理内存测试请求,并在不保留额外引用的前提下制造短期分配。 +/// public sealed class TestMemoryRequestHandler : IRequestHandler { + /// public ValueTask Handle(TestMemoryRequest request, CancellationToken cancellationToken) { // 模拟内存使用 @@ -430,25 +543,50 @@ public sealed class TestMemoryRequestHandler : IRequestHandler +/// 保存瞬态错误测试的共享计数状态。 +/// public static class TestTransientErrorHandler { + /// + /// 获取或设置当前已模拟的错误次数。 + /// public static int ErrorCount { get; set; } } +/// +/// 表示用于瞬态错误场景的测试请求。 +/// public sealed record TestTransientErrorRequest : IRequest { + /// + /// 获取或初始化允许连续抛出的最大错误次数。 + /// public int MaxErrors { get; init; } } +/// +/// 保存断路器场景的共享测试状态。 +/// public static class TestCircuitBreakerHandler { + /// + /// 获取或设置当前累计的失败次数。 + /// public static int FailureCount { get; set; } + + /// + /// 获取或设置当前累计的成功次数。 + /// public static int SuccessCount { get; set; } + + /// + /// 获取或设置断路器是否已处于打开状态。 + /// public static bool CircuitOpen { get; set; } /// - /// 重置断路器测试状态,避免静态字段在测试之间互相污染。 + /// 重置断路器测试状态,避免静态字段在测试之间互相污染。 /// public static void Reset() { @@ -458,50 +596,87 @@ public static class TestCircuitBreakerHandler } } +/// +/// 表示用于断路器场景的测试请求。 +/// public sealed record TestCircuitBreakerRequest : IRequest { + /// + /// 获取或初始化当前请求是否应主动模拟失败。 + /// public bool ShouldFail { get; init; } } -// 复杂场景相关类 +/// +/// 保存 Saga 执行与补偿过程中的共享状态。 +/// public class SagaData { /// - /// 获取 Saga 已成功执行的步骤集合。 + /// 获取 Saga 已成功执行的步骤集合。 /// public IList CompletedSteps { get; } = new List(); /// - /// 获取 Saga 失败后已执行补偿的步骤集合。 + /// 获取 Saga 失败后已执行补偿的步骤集合。 /// public IList CompensatedSteps { get; } = new List(); /// - /// 获取或设置 Saga 是否已经完整结束。 + /// 获取或设置 Saga 是否已经完整结束。 /// public bool IsCompleted { get; set; } } +/// +/// 表示 Saga 中单个步骤的测试请求。 +/// public sealed record TestSagaStepRequest : IRequest { + /// + /// 获取或初始化当前要执行的 Saga 步骤编号。 + /// public int Step { get; init; } + + /// + /// 获取或初始化当前 Saga 使用的共享状态对象。 + /// public SagaData SagaData { get; init; } = null!; + + /// + /// 获取或初始化当前步骤是否应模拟失败。 + /// public bool ShouldFail { get; init; } } +/// +/// 表示用于链式请求场景的起始请求。 +/// public sealed record TestChainStartRequest : IRequest; +/// +/// 表示依赖外部服务响应时间的测试请求。 +/// public sealed record TestExternalServiceRequest : IRequest { + /// + /// 获取或初始化模拟外部服务所需的响应时长,单位为毫秒。 + /// public int TimeoutMs { get; init; } } +/// +/// 表示用于模拟数据库写入的测试请求。 +/// public sealed record TestDatabaseRequest : IRequest { + /// + /// 获取或初始化要写入存储集合的数据内容。 + /// public string Data { get; init; } = string.Empty; /// - /// 获取或初始化用于模拟数据库写入的可变存储集合,同时避免泄漏具体集合实现。 + /// 获取或初始化用于模拟数据库写入的可变存储集合,同时避免泄漏具体集合实现。 /// public IList Storage { get; init; } = new List(); } diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs similarity index 56% rename from GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs rename to GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs index c0d1f260..d9ff2602 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsArchitectureContextIntegrationTests.cs @@ -9,15 +9,17 @@ using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; -namespace GFramework.Cqrs.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Cqrs; /// -/// Mediator与架构上下文集成测试 -/// 专注于测试Mediator在架构上下文中的集成和交互 +/// 验证 CQRS 请求分发与 的集成行为。 /// [TestFixture] -public class MediatorArchitectureIntegrationTests +public class CqrsArchitectureContextIntegrationTests { + /// + /// 初始化测试运行所需的容器、日志与架构上下文。 + /// [SetUp] public void SetUp() { @@ -28,21 +30,24 @@ public class MediatorArchitectureIntegrationTests var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance); loggerField?.SetValue(_container, - LoggerFactoryResolver.Provider.CreateLogger(nameof(MediatorArchitectureIntegrationTests))); + LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsArchitectureContextIntegrationTests))); - // 注册传统CQRS组件(用于混合模式测试) + // 注册传统 CQRS 组件,用于验证命令总线与请求分发可并存。 _commandBus = new CommandExecutor(); _container.RegisterPlurality(_commandBus); CqrsTestRuntime.RegisterHandlers( _container, - typeof(MediatorArchitectureIntegrationTests).Assembly, + typeof(CqrsArchitectureContextIntegrationTests).Assembly, typeof(ArchitectureContext).Assembly); _container.Freeze(); _context = new ArchitectureContext(_container); } + /// + /// 清理每个测试使用的容器与架构上下文引用。 + /// [TearDown] public void TearDown() { @@ -56,11 +61,13 @@ public class MediatorArchitectureIntegrationTests private ArchitectureContext? _context; + /// + /// 验证处理器可以观察到当前的架构上下文。 + /// [Test] public async Task Handler_Can_Access_Architecture_Context() { - // 由于我们没有实现实际的上下文访问,简化测试逻辑 - TestContextAwareHandler.LastContext = _context; // 直接设置 + TestContextAwareHandler.LastContext = null; var request = new TestContextAwareRequest(); await _context!.SendRequestAsync(request).ConfigureAwait(false); @@ -69,6 +76,9 @@ public class MediatorArchitectureIntegrationTests Assert.That(TestContextAwareHandler.LastContext, Is.SameAs(_context)); } + /// + /// 验证处理器能够通过当前上下文参与服务解析。 + /// [Test] public async Task Handler_Can_Retrieve_Services_From_Context() { @@ -81,11 +91,14 @@ public class MediatorArchitectureIntegrationTests Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.InstanceOf()); } + /// + /// 验证请求分发流程支持嵌套请求处理。 + /// [Test] public async Task Handler_Can_Send_Nested_Requests() { TestNestedRequestHandler2.ExecutionCount = 0; - var request = new TestNestedRequest { Depth = 1 }; // 简化为深度1 + var request = new TestNestedRequest { Depth = 1 }; var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); @@ -93,6 +106,9 @@ public class MediatorArchitectureIntegrationTests Assert.That(TestNestedRequestHandler2.ExecutionCount, Is.EqualTo(1)); } + /// + /// 验证请求处理期间的生命周期计数符合预期。 + /// [Test] public async Task Context_Lifecycle_Should_Be_Properly_Managed() { @@ -102,17 +118,20 @@ public class MediatorArchitectureIntegrationTests var request = new TestLifecycleRequest(); await _context!.SendRequestAsync(request).ConfigureAwait(false); - // 验证生命周期管理 + // 验证请求处理期间的初始化与释放计数符合预期。 Assert.That(TestLifecycleHandler.InitializationCount, Is.EqualTo(1)); Assert.That(TestLifecycleHandler.DisposalCount, Is.EqualTo(1)); } + /// + /// 验证并发请求使用的作用域彼此隔离。 + /// [Test] public async Task Scoped_Services_Should_Be_Properly_Isolated() { var results = new List(); - // 并发执行多个请求,每个请求都应该有自己的scope + // 并发执行多个请求,每个请求都应获得独立作用域。 var tasks = Enumerable.Range(0, 10) .Select(async i => { @@ -126,10 +145,13 @@ public class MediatorArchitectureIntegrationTests await Task.WhenAll(tasks).ConfigureAwait(false); - // 验证每个请求都得到了独立的scope实例 + // 验证每个请求都获得了独立的作用域结果。 Assert.That(results.Distinct().Count(), Is.EqualTo(10)); } + /// + /// 验证处理器抛出的异常会按原样传播到调用方。 + /// [Test] public async Task Context_Error_Should_Be_Properly_Propagated() { @@ -142,6 +164,9 @@ public class MediatorArchitectureIntegrationTests Assert.That(ex.Data["RequestId"], Is.Not.Null); } + /// + /// 验证处理器异常在记录后仍保持原始异常类型。 + /// [Test] public async Task Context_Should_Handle_Handler_Exceptions_Gracefully() { @@ -151,11 +176,14 @@ public class MediatorArchitectureIntegrationTests Assert.ThrowsAsync(async () => await _context!.SendRequestAsync(request).ConfigureAwait(false)); - // 验证异常被捕获和记录 + // 验证异常被捕获并保留原始类型。 Assert.That(TestExceptionHandler.LastException, Is.Not.Null); Assert.That(TestExceptionHandler.LastException, Is.InstanceOf()); } + /// + /// 验证架构上下文集成路径的额外分发开销保持在可接受范围内。 + /// [Test] public async Task Context_Overhead_Should_Be_Minimal() { @@ -172,11 +200,14 @@ public class MediatorArchitectureIntegrationTests stopwatch.Stop(); var avgTime = stopwatch.ElapsedMilliseconds / (double)iterations; - // 验证上下文集成的性能开销在合理范围内 + // 验证架构上下文集成的性能开销在合理范围内。 Assert.That(avgTime, Is.LessThan(5.0)); // 平均每个请求不超过5ms Console.WriteLine($"Average time with context integration: {avgTime:F2}ms"); } + /// + /// 验证缓存路径相较无缓存路径不会引入异常级别的额外开销。 + /// [Test] public async Task Context_Caching_Should_Improve_Performance() { @@ -184,7 +215,7 @@ public class MediatorArchitectureIntegrationTests var uncachedTimes = new List(); var cachedTimes = new List(); - // 测试无缓存情况 + // 测试无缓存路径。 for (int i = 0; i < iterations; i++) { var stopwatch = Stopwatch.StartNew(); @@ -194,7 +225,7 @@ public class MediatorArchitectureIntegrationTests uncachedTimes.Add(stopwatch.ElapsedMilliseconds); } - // 测试有缓存情况 + // 测试缓存命中路径。 for (int i = 0; i < iterations; i++) { var stopwatch = Stopwatch.StartNew(); @@ -207,11 +238,14 @@ public class MediatorArchitectureIntegrationTests var avgUncached = uncachedTimes.Average(); var avgCached = cachedTimes.Average(); - // 放宽性能要求 - Assert.That(avgCached, Is.LessThan(avgUncached * 2.5)); // 缓存应该更快 + // 放宽性能要求,避免环境抖动导致偶发失败。 + Assert.That(avgCached, Is.LessThan(avgUncached * 2.5)); Console.WriteLine($"Uncached avg: {avgUncached:F2}ms, Cached avg: {avgCached:F2}ms"); } + /// + /// 验证并发请求访问同一架构上下文时能够安全完成。 + /// [Test] public async Task Context_Should_Handle_Concurrent_Access_Safely() { @@ -232,14 +266,17 @@ public class MediatorArchitectureIntegrationTests var results = await Task.WhenAll(tasks).ConfigureAwait(false); - // 验证所有请求都成功完成 + // 验证所有请求都成功完成。 Assert.That(results.Length, Is.EqualTo(concurrentRequests)); Assert.That(results.Distinct().Count(), Is.EqualTo(concurrentRequests)); - // 验证执行顺序(应该大致按请求顺序) + // 验证每个请求都留下了执行痕迹。 Assert.That(executionOrder.Count, Is.EqualTo(concurrentRequests)); } + /// + /// 验证并发状态修改后共享状态仍保持一致。 + /// [Test] public async Task Context_State_Should_Remain_Consistent_Under_Concurrency() { @@ -259,14 +296,17 @@ public class MediatorArchitectureIntegrationTests await Task.WhenAll(tasks).ConfigureAwait(false); - // 验证最终状态正确(20个并发操作,每个+1) + // 验证最终状态正确。 Assert.That(sharedState.Counter, Is.EqualTo(concurrentOperations)); } + /// + /// 验证架构上下文可以与现有系统协同工作。 + /// [Test] public async Task Context_Can_Integrate_With_Existing_Systems() { - // 测试与现有系统的集成 + // 测试与现有系统的集成。 TestIntegrationHandler.LastSystemCall = null; var request = new TestIntegrationRequest(); @@ -276,24 +316,30 @@ public class MediatorArchitectureIntegrationTests Assert.That(TestIntegrationHandler.LastSystemCall, Is.EqualTo("System executed")); } + /// + /// 验证传统命令总线与请求响应式 CQRS 分发可以共存。 + /// [Test] public async Task Context_Can_Handle_Mixed_CQRS_Patterns() { - // 使用传统CQRS + // 使用传统 CQRS 命令总线。 var traditionalCommand = new TestTraditionalCommand(); _context!.SendCommand(traditionalCommand); - Assert.That(traditionalCommand.Executed, Is.True); // 这应该通过 + Assert.That(traditionalCommand.Executed, Is.True); - // 使用Mediator - var mediatorRequest = new TestMediatorRequest { Value = 42 }; - var result = await _context.SendRequestAsync(mediatorRequest).ConfigureAwait(false); + // 使用基于请求/响应的 CQRS 分发。 + var cqrsRequest = new TestCqrsRequest { Value = 42 }; + var result = await _context.SendRequestAsync(cqrsRequest).ConfigureAwait(false); Assert.That(result, Is.EqualTo(42)); - // 验证两者可以共存 + // 验证两种模式可以共存。 Assert.That(traditionalCommand.Executed, Is.True); Assert.That(result, Is.EqualTo(42)); } + /// + /// 验证上下文感知处理器在每次分发时都会获得新实例。 + /// [Test] public async Task ContextAware_Handler_Should_Use_A_Fresh_Instance_Per_Request() { @@ -307,19 +353,37 @@ public class MediatorArchitectureIntegrationTests Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context)); }); } - #region Integration Test Classes + #region Integration Test Types - public sealed class TestContextAwareRequestHandler : IRequestHandler + /// + /// 为上下文感知请求提供静态响应的测试处理器。 + /// + public sealed class TestContextAwareRequestHandler : ContextAwareBase, IRequestHandler { + /// + /// 记录当前处理器观察到的架构上下文,并返回固定结果。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 固定的测试结果。 public ValueTask Handle(TestContextAwareRequest request, CancellationToken cancellationToken) { - // 保持测试中设置的上下文,不要重置为null + TestContextAwareHandler.LastContext = Context; return new ValueTask("Context accessed"); } } + /// + /// 模拟从架构上下文中解析服务的测试处理器。 + /// public sealed class TestServiceRetrievalRequestHandler : IRequestHandler { + /// + /// 记录一次服务解析结果并返回固定响应。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 固定的测试结果。 public ValueTask Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken) { TestServiceRetrievalHandler.LastRetrievedService = new TestService(); @@ -327,38 +391,75 @@ public class MediatorArchitectureIntegrationTests } } + /// + /// 模拟嵌套请求处理的测试处理器。 + /// public sealed class TestNestedRequestHandler : IRequestHandler { + /// + /// 递增嵌套请求执行计数并返回深度描述。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 包含嵌套深度的固定结果。 public ValueTask Handle(TestNestedRequest request, CancellationToken cancellationToken) { TestNestedRequestHandler2.ExecutionCount++; - // 模拟嵌套调用 + // 模拟嵌套调用。 return new ValueTask($"Nested execution completed at depth {request.Depth}"); } } + /// + /// 模拟请求生命周期回调的测试处理器。 + /// public sealed class TestLifecycleRequestHandler : IRequestHandler { + /// + /// 递增初始化与释放计数来模拟生命周期管理。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 固定的测试结果。 public ValueTask Handle(TestLifecycleRequest request, CancellationToken cancellationToken) { TestLifecycleHandler.InitializationCount++; - // 模拟一些工作 + // 模拟一次完整处理流程中的工作。 TestLifecycleHandler.DisposalCount++; return new ValueTask("Lifecycle managed"); } } + /// + /// 返回请求编号以验证作用域隔离的测试处理器。 + /// public sealed class TestScopedServiceRequestHandler : IRequestHandler { + /// + /// 返回请求携带的编号。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 请求编号。 public ValueTask Handle(TestScopedServiceRequest request, CancellationToken cancellationToken) { - // 模拟返回请求ID + // 直接返回请求编号,便于验证不同请求的隔离性。 return new ValueTask(request.RequestId); } } + /// + /// 抛出携带附加数据的异常以验证错误传播的测试处理器。 + /// public sealed class TestErrorPropagationRequestHandler : IRequestHandler { + /// + /// 创建并抛出测试异常。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 该方法总是抛出异常,不返回结果。 + /// 始终抛出,用于验证异常透传。 public ValueTask Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken) { var ex = new InvalidOperationException("Test error from handler"); @@ -367,8 +468,18 @@ public class MediatorArchitectureIntegrationTests } } + /// + /// 抛出算术异常以验证异常捕获行为的测试处理器。 + /// public sealed class TestExceptionRequestHandler : IRequestHandler { + /// + /// 创建并抛出测试异常。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 该方法总是抛出异常,不返回结果。 + /// 始终抛出,用于验证异常记录行为。 public ValueTask Handle(TestExceptionRequest request, CancellationToken cancellationToken) { TestExceptionHandler.LastException = new DivideByZeroException("Test exception"); @@ -376,28 +487,55 @@ public class MediatorArchitectureIntegrationTests } } + /// + /// 提供轻量级请求处理以测量分发开销的测试处理器。 + /// public sealed class TestPerformanceRequest2Handler : IRequestHandler { + /// + /// 返回请求编号,避免额外逻辑干扰性能测量。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 请求编号。 public ValueTask Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken) { return new ValueTask(request.Id); } } + /// + /// 模拟无缓存慢路径的测试处理器。 + /// public sealed class TestUncachedRequestHandler : IRequestHandler { + /// + /// 人为引入延迟来模拟未命中缓存的处理路径。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 请求编号。 public async ValueTask Handle(TestUncachedRequest request, CancellationToken cancellationToken) { - // 模拟一些处理时间 + // 引入固定延迟,用于构造无缓存基线。 await Task.Delay(5, cancellationToken).ConfigureAwait(false); return request.Id; } } + /// + /// 使用静态缓存模拟可复用处理结果的测试处理器。 + /// public sealed class TestCachedRequestHandler : IRequestHandler { private static readonly ConcurrentDictionary _cache = new(); + /// + /// 优先返回缓存结果,未命中时执行较慢路径并写入缓存。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 请求编号。 public async ValueTask Handle(TestCachedRequest request, CancellationToken cancellationToken) { if (_cache.TryGetValue(request.Id, out var cachedValue)) @@ -405,14 +543,23 @@ public class MediatorArchitectureIntegrationTests return cachedValue; } - // 模拟处理时间 + // 模拟首次处理成本。 await Task.Delay(10, cancellationToken).ConfigureAwait(false); return _cache.GetOrAdd(request.Id, static id => id); } } + /// + /// 记录并发请求执行顺序的测试处理器。 + /// public sealed class TestConcurrentRequestHandler : IRequestHandler { + /// + /// 将请求编号记录到共享顺序跟踪器中。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 请求编号。 public ValueTask Handle(TestConcurrentRequest request, CancellationToken cancellationToken) { lock (request.OrderTracker) @@ -424,8 +571,17 @@ public class MediatorArchitectureIntegrationTests } } + /// + /// 修改共享状态以验证并发一致性的测试处理器。 + /// public sealed class TestStateModificationRequestHandler : IRequestHandler { + /// + /// 将请求中的增量写入共享状态。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 固定的测试结果。 public ValueTask Handle(TestStateModificationRequest request, CancellationToken cancellationToken) { request.SharedState.IncrementBy(request.Increment); @@ -433,8 +589,17 @@ public class MediatorArchitectureIntegrationTests } } + /// + /// 模拟与既有系统交互的测试处理器。 + /// public sealed class TestIntegrationRequestHandler : IRequestHandler { + /// + /// 记录一次系统调用并返回成功结果。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 固定的成功结果。 public ValueTask Handle(TestIntegrationRequest request, CancellationToken cancellationToken) { TestIntegrationHandler.LastSystemCall = "System executed"; @@ -442,9 +607,18 @@ public class MediatorArchitectureIntegrationTests } } - public sealed class TestMediatorRequestHandler : IRequestHandler + /// + /// 为请求/响应分发路径返回固定编号的测试处理器。 + /// + public sealed class TestCqrsRequestHandler : IRequestHandler { - public ValueTask Handle(TestMediatorRequest request, CancellationToken cancellationToken) + /// + /// 返回请求中的值,验证 CQRS 请求分发路径可用。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 请求中携带的值。 + public ValueTask Handle(TestCqrsRequest request, CancellationToken cancellationToken) { return new ValueTask(request.Value); } @@ -461,7 +635,14 @@ public class MediatorArchitectureIntegrationTests private static readonly List TrackedInstanceIds = []; private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId); + /// + /// 获取按请求记录的架构上下文序列。 + /// public static IReadOnlyList Contexts => TrackedContexts; + + /// + /// 获取已观察到的处理器实例编号序列。 + /// public static IReadOnlyList SeenInstanceIds => TrackedInstanceIds; /// @@ -488,110 +669,235 @@ public class MediatorArchitectureIntegrationTests } } + /// + /// 用于验证处理器可观察到当前架构上下文的测试请求。 + /// public sealed record TestContextAwareRequest : IRequest; + /// + /// 保存最近一次上下文观察结果的测试状态容器。 + /// public static class TestContextAwareHandler { + /// + /// 获取或设置最近一次测试观察到的架构上下文。 + /// public static IArchitectureContext? LastContext { get; set; } } + /// + /// 用于验证服务解析流程的测试请求。 + /// public sealed record TestServiceRetrievalRequest : IRequest; + /// + /// 保存最近一次服务解析结果的测试状态容器。 + /// public static class TestServiceRetrievalHandler { + /// + /// 获取或设置最近一次解析得到的服务实例。 + /// public static object? LastRetrievedService { get; set; } } + /// + /// 表示用于验证服务解析的简单测试服务。 + /// public class TestService { + /// + /// 获取当前测试服务实例的唯一标识。 + /// public string Id { get; } = Guid.NewGuid().ToString(); } + /// + /// 用于验证嵌套请求处理的测试请求。 + /// public sealed record TestNestedRequest : IRequest { + /// + /// 获取请求携带的嵌套深度。 + /// public int Depth { get; init; } } + /// + /// 保存嵌套请求执行计数的测试状态容器。 + /// public static class TestNestedRequestHandler2 { + /// + /// 获取或设置嵌套请求处理器的执行次数。 + /// public static int ExecutionCount { get; set; } } - // 生命周期相关类 + /// + /// 用于验证生命周期管理的测试请求。 + /// public sealed record TestLifecycleRequest : IRequest; + /// + /// 保存生命周期计数的测试状态容器。 + /// public static class TestLifecycleHandler { + /// + /// 获取或设置初始化次数。 + /// public static int InitializationCount { get; set; } + + /// + /// 获取或设置释放次数。 + /// public static int DisposalCount { get; set; } } + /// + /// 用于验证作用域隔离的测试请求。 + /// public sealed record TestScopedServiceRequest : IRequest { + /// + /// 获取请求编号。 + /// public int RequestId { get; init; } } - // 错误处理相关类 + /// + /// 用于验证异常传播的测试请求。 + /// public sealed record TestErrorPropagationRequest : IRequest; + /// + /// 保存最近一次异常实例的测试状态容器。 + /// public static class TestExceptionHandler { + /// + /// 获取或设置最近一次记录到的异常。 + /// public static Exception? LastException { get; set; } } + /// + /// 用于验证异常记录行为的测试请求。 + /// public sealed record TestExceptionRequest : IRequest; - // 性能测试相关类 + /// + /// 用于验证轻量请求分发开销的测试请求。 + /// public sealed record TestPerformanceRequest2 : IRequest { + /// + /// 获取请求编号。 + /// public int Id { get; init; } } + /// + /// 用于验证未缓存处理路径的测试请求。 + /// public sealed record TestUncachedRequest : IRequest { + /// + /// 获取请求编号。 + /// public int Id { get; init; } } + /// + /// 用于验证缓存处理路径的测试请求。 + /// public sealed record TestCachedRequest : IRequest { + /// + /// 获取请求编号。 + /// public int Id { get; init; } } - // 并发测试相关类 + /// + /// 表示并发测试共享的可变状态。 + /// public class SharedState { private int _counter; + /// + /// 获取当前计数值。 + /// public int Counter => _counter; + /// + /// 以线程安全方式增加计数器。 + /// + /// 要增加的数值。 public void IncrementBy(int increment) { Interlocked.Add(ref _counter, increment); } } + /// + /// 用于验证并发请求调度安全性的测试请求。 + /// public sealed record TestConcurrentRequest : IRequest { + /// + /// 获取请求编号。 + /// public int RequestId { get; init; } + + /// + /// 获取用于记录执行顺序的共享集合。 + /// public ICollection OrderTracker { get; init; } = new List(); } + /// + /// 用于验证并发状态修改一致性的测试请求。 + /// public sealed record TestStateModificationRequest : IRequest { + /// + /// 获取待修改的共享状态实例。 + /// public SharedState SharedState { get; init; } = null!; + + /// + /// 获取要增加的计数值。 + /// public int Increment { get; init; } } - // 集成测试相关类 + /// + /// 保存最近一次系统调用结果的测试状态容器。 + /// public static class TestIntegrationHandler { + /// + /// 获取或设置最近一次系统调用记录。 + /// public static string? LastSystemCall { get; set; } } + /// + /// 用于验证系统集成行为的测试请求。 + /// public sealed record TestIntegrationRequest : IRequest; - public sealed record TestMediatorRequest : IRequest + /// + /// 用于验证请求/响应 CQRS 分发路径的测试请求。 + /// + public sealed record TestCqrsRequest : IRequest { + /// + /// 获取请求返回的测试值。 + /// public int Value { get; init; } } @@ -600,17 +906,33 @@ public class MediatorArchitectureIntegrationTests /// public sealed record TestPerDispatchContextAwareRequest : IRequest; - // 传统命令用于混合测试 + /// + /// 表示用于混合模式验证的传统命令。 + /// public class TestTraditionalCommand : ICommand { + /// + /// 获取命令是否已执行。 + /// public bool Executed { get; private set; } + /// + /// 将命令标记为已执行。 + /// public void Execute() => Executed = true; + /// + /// 为兼容命令接口保留上下文设置入口,当前测试无需使用。 + /// + /// 命令上下文。 public void SetContext(IArchitectureContext context) { } + /// + /// 返回命令上下文占位值,当前测试路径不会消费该结果。 + /// + /// 始终返回空引用占位值。 public IArchitectureContext GetContext() => null!; } diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs new file mode 100644 index 00000000..e5e5171f --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -0,0 +1,152 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Architectures; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 generated request invoker provider 的 registrar 接线与 dispatcher 消费语义。 +/// +[TestFixture] +[NonParallelizable] +internal sealed class CqrsGeneratedRequestInvokerProviderTests +{ + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + + /// + /// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。 + /// + [SetUp] + public void SetUp() + { + _previousLoggerFactoryProvider = LoggerFactoryResolver.Provider; + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + ClearRegistrarCaches(); + ClearDispatcherCaches(); + } + + /// + /// 在每个用例后清理静态缓存。 + /// + [TearDown] + public void TearDown() + { + LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider ?? new ConsoleLoggerFactoryProvider(); + ClearRegistrarCaches(); + ClearDispatcherCaches(); + } + + /// + /// 验证 registrar 激活 generated registry 后,会把 request invoker provider 注册到容器中。 + /// + [Test] + public void RegisterHandlers_Should_Register_Generated_Request_Invoker_Provider() + { + var generatedAssembly = CreateGeneratedRequestInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var providers = container.GetAll(); + + Assert.That( + providers.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); + } + + /// + /// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。 + /// + [Test] + public async Task SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered() + { + var generatedAssembly = CreateGeneratedRequestInvokerAssembly(); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")); + Assert.That(response, Is.EqualTo("generated:payload")); + } + + /// + /// 创建带有 generated request invoker registry 元数据的程序集替身。 + /// + private static Mock CreateGeneratedRequestInvokerAssembly() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Cqrs.Tests.Cqrs.GeneratedRequestInvokerAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedRequestInvokerProviderRegistry))]); + return generatedAssembly; + } + + /// + /// 清空 registrar 静态缓存。 + /// + private static void ClearRegistrarCaches() + { + ClearCache(GetRegistrarCacheField("AssemblyMetadataCache")); + ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache")); + ClearCache(GetRegistrarCacheField("LoadableTypesCache")); + ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache")); + } + + /// + /// 清空 dispatcher 静态缓存。 + /// + private static void ClearDispatcherCaches() + { + ClearCache(GetDispatcherCacheField("NotificationDispatchBindings")); + ClearCache(GetDispatcherCacheField("RequestDispatchBindings")); + ClearCache(GetDispatcherCacheField("StreamDispatchBindings")); + ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers")); + } + + /// + /// 通过反射读取 registrar 的静态缓存字段。 + /// + private static object GetRegistrarCacheField(string fieldName) + { + var field = typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)! + .GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}."); + return field!.GetValue(null) + ?? throw new InvalidOperationException($"Registrar cache field {fieldName} returned null."); + } + + /// + /// 通过反射读取 dispatcher 的静态缓存字段。 + /// + private static object GetDispatcherCacheField(string fieldName) + { + var field = typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)! + .GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}."); + return field!.GetValue(null) + ?? throw new InvalidOperationException($"Dispatcher cache field {fieldName} returned null."); + } + + /// + /// 清空目标缓存实例。 + /// + private static void ClearCache(object cache) + { + _ = cache.GetType() + .GetMethod("Clear", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)! + .Invoke(cache, Array.Empty()); + } + +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs new file mode 100644 index 00000000..b069be94 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs @@ -0,0 +1,326 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Architectures; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Cqrs; +using GFramework.Cqrs.Notification; +using GFramework.Cqrs.Tests.Logging; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证默认 CQRS runtime 的通知发布策略接缝。 +/// +[TestFixture] +internal sealed class CqrsNotificationPublisherTests +{ + /// + /// 验证当调用方显式提供自定义通知发布器时,dispatcher 会按该发布器定义的顺序执行处理器。 + /// + [Test] + public async Task PublishAsync_Should_Use_Custom_NotificationPublisher_When_Runtime_Is_Created_With_It() + { + var invocationOrder = new List(); + var handlers = new object[] + { + new RecordingNotificationHandler("first", invocationOrder), + new RecordingNotificationHandler("second", invocationOrder) + }; + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns(handlers); + }, + new ReverseOrderNotificationPublisher()); + + await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false); + + Assert.That(invocationOrder, Is.EqualTo(["second", "first"])); + } + + /// + /// 验证当容器在 runtime 创建前已显式注册自定义通知发布器时, + /// `RegisterInfrastructure` 这条默认接线会复用该策略。 + /// + [Test] + public async Task RegisterInfrastructure_Should_Use_PreRegistered_NotificationPublisher() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + + var container = new MicrosoftDiContainer(); + var publisher = new TrackingNotificationPublisher(); + container.Register(publisher); + container.Register>(new RecordingNotificationHandler("only", [])); + CqrsTestRuntime.RegisterInfrastructure(container); + container.Freeze(); + + var context = new ArchitectureContext(container); + + await context.PublishAsync(new PublisherNotification()).ConfigureAwait(false); + + Assert.That(publisher.WasCalled, Is.True); + } + + /// + /// 验证自定义通知发布器通过发布上下文回调执行处理器时,dispatcher 仍会在调用前注入当前架构上下文。 + /// + [Test] + public async Task PublishAsync_Should_Prepare_Context_Before_Custom_Publisher_Invokes_Handler() + { + var handler = new ContextAwarePublisherTestHandler(); + var architectureContext = new Mock(MockBehavior.Strict); + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns([handler]); + }, + new PassthroughNotificationPublisher()); + + await runtime.PublishAsync(architectureContext.Object, new PublisherNotification()).ConfigureAwait(false); + + Assert.That(handler.ObservedContext, Is.SameAs(architectureContext.Object)); + } + + /// + /// 验证默认通知发布器在零处理器场景下会保持静默完成。 + /// + [Test] + public void PublishAsync_Should_Complete_When_No_Handlers_Are_Registered() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns(Array.Empty()); + }); + + Assert.That( + async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false), + Throws.Nothing); + } + + /// + /// 验证默认通知发布器会保持“首个异常立即中断后续处理器”的既有语义。 + /// + [Test] + public void PublishAsync_Should_Stop_After_First_Handler_Exception_When_Using_Default_Publisher() + { + var trailingHandler = new RecordingNotificationHandler("second", []); + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) + .Returns( + [ + new ThrowingNotificationHandler(), + trailingHandler + ]); + }); + + Assert.That( + async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false), + Throws.InvalidOperationException.With.Message.EqualTo("boom")); + Assert.That(trailingHandler.Invoked, Is.False); + } + + /// + /// 创建一个只满足当前测试最小依赖面的 dispatcher runtime。 + /// + /// 对容器 mock 的额外配置。 + /// 要注入的自定义通知发布器;若为 则使用默认发布器。 + /// 默认 CQRS runtime。 + private static GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime CreateRuntime( + Action> configureContainer, + INotificationPublisher? notificationPublisher = null) + { + var container = new Mock(MockBehavior.Strict); + var logger = new TestLogger(nameof(CqrsNotificationPublisherTests), LogLevel.Debug); + + configureContainer(container); + return CqrsRuntimeFactory.CreateRuntime(container.Object, logger, notificationPublisher); + } + + /// + /// 为当前测试提供最小的 CQRS 上下文标记。 + /// + private sealed class FakeCqrsContext : ICqrsContext + { + } + + /// + /// 为通知发布器测试提供最小通知类型。 + /// + private sealed record PublisherNotification : INotification; + + /// + /// 按传入顺序直接执行处理器的测试发布器。 + /// + private sealed class PassthroughNotificationPublisher : INotificationPublisher + { + /// + /// 按当前处理器集合顺序执行所有处理器。 + /// + /// 通知类型。 + /// 当前发布上下文。 + /// 取消令牌。 + /// 表示通知发布完成的值任务。 + public async ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + foreach (var handler in context.Handlers) + { + await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false); + } + } + } + + /// + /// 按逆序执行处理器的测试发布器,用于证明 dispatcher 已真正委托给自定义策略。 + /// + private sealed class ReverseOrderNotificationPublisher : INotificationPublisher + { + /// + /// 按逆序执行当前发布上下文中的所有处理器。 + /// + /// 通知类型。 + /// 当前发布上下文。 + /// 取消令牌。 + /// 表示通知发布完成的值任务。 + public async ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + for (var index = context.Handlers.Count - 1; index >= 0; index--) + { + await context.InvokeHandlerAsync(context.Handlers[index], cancellationToken).ConfigureAwait(false); + } + } + } + + /// + /// 仅记录自身是否被调用的测试发布器,用于验证默认接线是否已接管到自定义策略。 + /// + private sealed class TrackingNotificationPublisher : INotificationPublisher + { + /// + /// 获取当前发布器是否至少执行过一次发布。 + /// + public bool WasCalled { get; private set; } + + /// + /// 记录当前发布器已被调用,并继续按当前顺序执行所有处理器。 + /// + /// 通知类型。 + /// 当前发布上下文。 + /// 取消令牌。 + /// 表示通知发布完成的值任务。 + public async ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + WasCalled = true; + + foreach (var handler in context.Handlers) + { + await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false); + } + } + } + + /// + /// 记录调用顺序的最小通知处理器。 + /// + private sealed class RecordingNotificationHandler : INotificationHandler + { + private readonly List _invocationOrder; + private readonly string _name; + + /// + /// 初始化一个记录调用顺序的测试处理器。 + /// + /// 当前处理器对应的名称。 + /// 承载调用顺序的列表。 + public RecordingNotificationHandler(string name, List invocationOrder) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(invocationOrder); + + _name = name; + _invocationOrder = invocationOrder; + } + + /// + /// 获取当前处理器是否已被调用。 + /// + public bool Invoked { get; private set; } + + /// + /// 把当前处理器名称追加到调用顺序列表。 + /// + /// 当前通知。 + /// 取消令牌。 + /// 已完成的值任务。 + public ValueTask Handle(PublisherNotification notification, CancellationToken cancellationToken) + { + Invoked = true; + _invocationOrder.Add(_name); + return ValueTask.CompletedTask; + } + } + + /// + /// 在被调用时主动抛出异常的测试处理器。 + /// + private sealed class ThrowingNotificationHandler : INotificationHandler + { + /// + /// 抛出固定异常,验证默认发布器的失败即停语义。 + /// + /// 当前通知。 + /// 取消令牌。 + /// 不会成功返回。 + /// 始终抛出,表示当前处理器失败。 + public ValueTask Handle(PublisherNotification notification, CancellationToken cancellationToken) + { + throw new InvalidOperationException("boom"); + } + } + + /// + /// 记录 dispatcher 是否在自定义发布器路径中完成上下文注入的测试处理器。 + /// + private sealed class ContextAwarePublisherTestHandler + : CqrsContextAwareHandlerBase, + INotificationHandler + { + /// + /// 获取当前处理器在执行时观察到的架构上下文。 + /// + public IArchitectureContext? ObservedContext { get; private set; } + + /// + /// 记录当前执行时观察到的架构上下文。 + /// + /// 当前通知。 + /// 取消令牌。 + /// 已完成的值任务。 + public ValueTask Handle(PublisherNotification notification, CancellationToken cancellationToken) + { + ObservedContext = Context; + return ValueTask.CompletedTask; + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs new file mode 100644 index 00000000..bad287b6 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerProviderRegistry.cs @@ -0,0 +1,90 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 模拟同时提供 handler 注册与 request invoker 元数据的 generated registry。 +/// +internal sealed class GeneratedRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors +{ + private static readonly CqrsRequestInvokerDescriptor Descriptor = new( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!); + + private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + Descriptor); + + /// + /// 将测试请求处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRequestInvokerRequestHandler).FullName} as {typeof(IRequestHandler).FullName}."); + } + + /// + /// 尝试返回指定 request/response 类型对对应的 generated invoker 描述符。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 命中时返回的描述符。 + /// 若类型对匹配当前测试请求则返回 + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 返回当前 registry 暴露的全部 generated request invoker 描述符。 + /// + /// 单条测试 request invoker 描述符条目。 + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + /// + /// 模拟 generated request invoker 直接执行后的返回值。 + /// + /// 当前请求处理器实例。 + /// 当前测试请求。 + /// 取消令牌。 + /// 带有 generated 前缀的结果,便于断言 dispatcher 走了 provider 路径。 + private static ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + _ = handler as IRequestHandler + ?? throw new InvalidOperationException("Generated invoker received an incompatible handler instance."); + var typedRequest = (GeneratedRequestInvokerRequest)request; + return ValueTask.FromResult($"generated:{typedRequest.Value}"); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs new file mode 100644 index 00000000..ce07f7ac --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequest.cs @@ -0,0 +1,9 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 用于验证 generated request invoker provider 接线的测试请求。 +/// +/// 用于验证 generated invoker 结果拼接的请求负载。 +internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest; diff --git a/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs new file mode 100644 index 00000000..2127b216 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/GeneratedRequestInvokerRequestHandler.cs @@ -0,0 +1,21 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 供 generated request invoker provider 测试使用的请求处理器。 +/// +internal sealed class GeneratedRequestInvokerRequestHandler : IRequestHandler +{ + /// + /// 返回带有运行时处理器前缀的结果,便于和 generated invoker 自定义结果区分。 + /// + /// 当前测试请求。 + /// 取消令牌。 + /// 运行时处理器生成的响应字符串。 + public ValueTask Handle(GeneratedRequestInvokerRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + return ValueTask.FromResult($"runtime:{request.Value}"); + } +} diff --git a/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs b/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs new file mode 100644 index 00000000..cc7cce14 --- /dev/null +++ b/GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 描述单个 request/response 类型对在运行时分发时需要复用的元数据。 +/// +/// 当前请求处理器在容器中的服务类型。 +/// +/// 执行单个请求处理器的开放静态方法。 +/// dispatcher 会在首次创建 request binding 时,把该方法绑定成内部使用的强类型委托。 +/// +/// +/// dispatcher 会继续自行构造 pipeline behavior 服务类型并负责上下文注入; +/// 该描述符只前移请求处理器服务类型与直接调用方法元数据。 +/// +public sealed class CqrsRequestInvokerDescriptor( + Type handlerType, + MethodInfo invokerMethod) +{ + /// + /// 获取请求处理器在容器中的服务类型。 + /// + public Type HandlerType { get; } = handlerType ?? throw new ArgumentNullException(nameof(handlerType)); + + /// + /// 获取执行请求处理器的开放静态方法。 + /// + public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod)); +} diff --git a/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs b/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs new file mode 100644 index 00000000..679652b6 --- /dev/null +++ b/GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs @@ -0,0 +1,12 @@ +namespace GFramework.Cqrs; + +/// +/// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。 +/// +/// 请求运行时类型。 +/// 响应运行时类型。 +/// 对应的 generated request invoker 描述符。 +public sealed record CqrsRequestInvokerDescriptorEntry( + Type RequestType, + Type ResponseType, + CqrsRequestInvokerDescriptor Descriptor); diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs index cbed68aa..32433f88 100644 --- a/GFramework.Cqrs/CqrsRuntimeFactory.cs +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -2,6 +2,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Internal; +using GFramework.Cqrs.Notification; namespace GFramework.Cqrs; @@ -24,11 +25,32 @@ public static class CqrsRuntimeFactory /// 。 /// public static ICqrsRuntime CreateRuntime(IIocContainer container, ILogger logger) + { + return CreateRuntime(container, logger, notificationPublisher: null); + } + + /// + /// 创建默认 CQRS runtime 分发器,并允许调用方指定通知发布策略。 + /// + /// 目标依赖注入容器。 + /// 用于 runtime 诊断的日志器。 + /// 可选的通知发布策略;若为 则使用默认顺序发布器。 + /// 默认 CQRS runtime。 + /// + /// 。 + /// + public static ICqrsRuntime CreateRuntime( + IIocContainer container, + ILogger logger, + INotificationPublisher? notificationPublisher) { ArgumentNullException.ThrowIfNull(container); ArgumentNullException.ThrowIfNull(logger); - return new CqrsDispatcher(container, logger); + return new CqrsDispatcher( + container, + logger, + notificationPublisher ?? new SequentialNotificationPublisher()); } /// diff --git a/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs b/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs new file mode 100644 index 00000000..56b74741 --- /dev/null +++ b/GFramework.Cqrs/ICqrsRequestInvokerProvider.cs @@ -0,0 +1,33 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs; + +/// +/// 定义由源码生成器或手写注册器提供的 request invoker 元数据契约。 +/// +/// +/// 该 seam 允许运行时在首次创建 request dispatch binding 时, +/// 直接复用编译期已知的请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。 +/// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射绑定创建路径。 +/// 当前默认 runtime 通过 在注册阶段一次性读取并缓存 +/// provider 暴露的描述符; +/// 主要用于 provider 自检、测试和显式调用场景,而不是 dispatcher 在分发热路径上的二次回调入口。 +/// +public interface ICqrsRequestInvokerProvider +{ + /// +/// 尝试为指定请求/响应类型对提供运行时元数据。 +/// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 命中时返回的 request invoker 元数据。 + /// 若当前 provider 可处理该请求/响应类型对则返回 ;否则返回 + /// + /// 若 provider 希望被默认 runtime 自动接线到 dispatcher 的 generated invoker 缓存中, + /// 还必须同时实现 ,以便 registrar 在注册阶段枚举全部描述符。 + /// + bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor); +} diff --git a/GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs b/GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs new file mode 100644 index 00000000..0aa08698 --- /dev/null +++ b/GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs; + +/// +/// 为 generated request invoker provider 暴露可枚举描述符集合的内部辅助契约。 +/// +/// +/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 request invoker 描述符, +/// 并把它们登记到 dispatcher 的进程级弱缓存中。 +/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。 +/// +public interface IEnumeratesCqrsRequestInvokerDescriptors +{ + /// + /// 返回当前 provider 可声明的全部 request invoker 描述符条目。 + /// + /// 按 provider 定义顺序枚举的描述符条目集合。 + IReadOnlyList GetDescriptors(); +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index d604beac..a88be732 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -4,6 +4,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Notification; using ICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Cqrs.Internal; @@ -14,8 +15,14 @@ namespace GFramework.Cqrs.Internal; /// internal sealed class CqrsDispatcher( IIocContainer container, - ILogger logger) : ICqrsRuntime + ILogger logger, + INotificationPublisher notificationPublisher) : ICqrsRuntime { + // 卸载安全的进程级缓存:当 generated registry 提供 request invoker 元数据时, + // registrar 会按请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。 + private static readonly WeakTypePairCache + GeneratedRequestInvokers = new(); + // 卸载安全的进程级缓存:通知类型只以弱键语义保留。 // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。 private static readonly WeakKeyCache @@ -43,6 +50,10 @@ internal sealed class CqrsDispatcher( private static readonly MethodInfo StreamHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) .GetMethod(nameof(InvokeStreamHandler), BindingFlags.NonPublic | BindingFlags.Static)!; + private readonly INotificationPublisher _notificationPublisher = notificationPublisher + ?? throw new ArgumentNullException( + nameof(notificationPublisher)); + /// /// 发布通知到所有已注册处理器。 /// @@ -71,11 +82,8 @@ internal sealed class CqrsDispatcher( return; } - foreach (var handler in handlers) - { - PrepareHandler(handler, context); - await dispatchBinding.Invoker(handler, notification, cancellationToken).ConfigureAwait(false); - } + var publishContext = CreateNotificationPublishContext(notification, handlers, context, dispatchBinding.Invoker); + await _notificationPublisher.PublishAsync(publishContext, cancellationToken).ConfigureAwait(false); } /// @@ -166,6 +174,17 @@ internal sealed class CqrsDispatcher( /// private static RequestDispatchBinding CreateRequestDispatchBinding(Type requestType) { + var generatedDescriptor = TryGetGeneratedRequestInvokerDescriptor(requestType); + if (generatedDescriptor is not null) + { + var resolvedGeneratedDescriptor = generatedDescriptor.Value; + return new RequestDispatchBinding( + resolvedGeneratedDescriptor.HandlerType, + typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), + resolvedGeneratedDescriptor.Invoker, + requestType); + } + return new RequestDispatchBinding( typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)), typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), @@ -200,6 +219,48 @@ internal sealed class CqrsDispatcher( return RequestDispatchBindingBox.Create(CreateRequestDispatchBinding(requestType)); } + /// + /// 尝试从容器已注册的 generated request invoker provider 中获取指定请求/响应类型对的元数据。 + /// + /// 当前请求响应类型。 + /// 请求运行时类型。 + /// 命中时返回强类型化后的描述符;否则返回 + private static RequestInvokerDescriptor? TryGetGeneratedRequestInvokerDescriptor(Type requestType) + { + return GeneratedRequestInvokers.TryGetValue(requestType, typeof(TResponse), out var metadata) && + metadata is not null + ? CreateRequestInvokerDescriptor(requestType, metadata) + : null; + } + + /// + /// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的强类型 request invoker 描述符。 + /// + /// 当前请求响应类型。 + /// 请求运行时类型。 + /// provider 返回的弱类型描述符。 + /// 可直接用于创建 request dispatch binding 的强类型描述符。 + /// 当 provider 返回的委托签名与当前请求/响应类型对不匹配时抛出。 + private static RequestInvokerDescriptor CreateRequestInvokerDescriptor( + Type requestType, + GeneratedRequestInvokerMetadata descriptor) + { + if (!descriptor.InvokerMethod.IsStatic) + { + throw new InvalidOperationException( + $"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); + } + + if (Delegate.CreateDelegate(typeof(RequestInvoker), descriptor.InvokerMethod) is not + RequestInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); + } + + return new RequestInvokerDescriptor(descriptor.HandlerType, invoker); + } + /// /// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 /// @@ -240,6 +301,50 @@ internal sealed class CqrsDispatcher( return (NotificationInvoker)Delegate.CreateDelegate(typeof(NotificationInvoker), method); } + /// + /// 为当前通知发布调用创建发布上下文,把处理器集合与执行入口收敛到同一对象。 + /// + /// 通知类型。 + /// 当前通知。 + /// 当前发布调用已解析到的处理器集合。 + /// 当前 CQRS 分发上下文。 + /// 执行单个通知处理器时复用的强类型调用委托。 + /// 供通知发布器消费的执行上下文。 + private static NotificationPublishContext CreateNotificationPublishContext( + TNotification notification, + IReadOnlyList handlers, + ICqrsContext context, + NotificationInvoker invoker) + where TNotification : INotification + { + return new DelegatingNotificationPublishContext( + notification, + handlers, + new NotificationDispatchState(context, invoker), + static (handler, currentNotification, state, currentCancellationToken) => + InvokePublishedNotificationHandlerAsync(handler, currentNotification, state, currentCancellationToken)); + } + + /// + /// 执行通知发布器选中的单个处理器,并在调用前注入当前分发上下文。 + /// + /// 通知类型。 + /// 要执行的处理器实例。 + /// 当前通知。 + /// 当前处理器执行所需的 dispatcher 状态。 + /// 取消令牌。 + /// 表示当前处理器执行完成的值任务。 + private static ValueTask InvokePublishedNotificationHandlerAsync( + object handler, + TNotification notification, + NotificationDispatchState state, + CancellationToken cancellationToken) + where TNotification : INotification + { + PrepareHandler(handler, state.Context); + return state.Invoker(handler, notification!, cancellationToken); + } + /// /// 生成流式处理器调用委托,避免每次创建流都重复反射。 /// @@ -387,6 +492,15 @@ internal sealed class CqrsDispatcher( public NotificationInvoker Invoker { get; } = invoker; } + /// + /// 保存通知发布器执行单个 handler 时需要复用的 dispatcher 状态。 + /// + /// 当前 CQRS 分发上下文。 + /// 执行单个通知处理器的强类型调用委托。 + private readonly record struct NotificationDispatchState( + ICqrsContext Context, + NotificationInvoker Invoker); + /// /// 保存流式请求分发路径所需的服务类型与调用委托。 /// 该绑定让建流热路径只需一次缓存命中即可获得解析与调用所需元数据。 @@ -523,6 +637,46 @@ internal sealed class CqrsDispatcher( private readonly record struct RequestPipelineExecutorFactoryState( RequestPipelineInvoker PipelineInvoker); + /// + /// 记录 registrar 写入的 generated request invoker 元数据。 + /// + /// 请求处理器在容器中的服务类型。 + /// 执行请求处理器的开放静态方法。 + private sealed record GeneratedRequestInvokerMetadata( + Type HandlerType, + MethodInfo InvokerMethod); + + /// + /// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。 + /// + /// 当前请求响应类型。 + private readonly record struct RequestInvokerDescriptor( + Type HandlerType, + RequestInvoker Invoker); + + /// + /// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。 + /// + /// 请求运行时类型。 + /// 响应运行时类型。 + /// 要登记的 generated request invoker 描述符。 + internal static void RegisterGeneratedRequestInvokerDescriptor( + Type requestType, + Type responseType, + CqrsRequestInvokerDescriptor descriptor) + { + ArgumentNullException.ThrowIfNull(requestType); + ArgumentNullException.ThrowIfNull(responseType); + ArgumentNullException.ThrowIfNull(descriptor); + + _ = GeneratedRequestInvokers.GetOrAdd( + requestType, + responseType, + (_, _) => new GeneratedRequestInvokerMetadata( + descriptor.HandlerType, + descriptor.InvokerMethod)); + } + /// /// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。 /// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。 diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 1be2729c..f6e88c1d 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -239,6 +239,62 @@ internal static class CqrsHandlerRegistrar logger.Debug( $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); registry.Register(services, logger); + RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger); + } + } + + /// + /// 当 generated registry 同时提供 request invoker 元数据时,把该 provider 注册到当前容器中。 + /// + /// 目标服务集合。 + /// 当前已激活的 generated registry。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// provider 作为 registry 的附加能力注册到容器后,dispatcher 才能在首次请求分发时优先消费编译期生成的 invoker 元数据。 + /// 若 registry 不实现该契约,则保持现有纯反射 request binding 创建语义。 + /// + private static void RegisterGeneratedRequestInvokerProvider( + IServiceCollection services, + ICqrsHandlerRegistry registry, + string assemblyName, + ILogger logger) + { + if (registry is not ICqrsRequestInvokerProvider provider) + return; + + RegisterGeneratedRequestInvokerDescriptors(provider, assemblyName, logger); + services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider); + logger.Debug( + $"Registered CQRS request invoker provider {provider.GetType().FullName} for assembly {assemblyName}."); + } + + /// + /// 读取 generated request invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。 + /// + /// 当前已激活的 request invoker provider。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// + /// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。 + /// 这样 request dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。 + /// + private static void RegisterGeneratedRequestInvokerDescriptors( + ICqrsRequestInvokerProvider provider, + string assemblyName, + ILogger logger) + { + if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource) + return; + + foreach (var descriptorEntry in descriptorSource.GetDescriptors()) + { + CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor( + descriptorEntry.RequestType, + descriptorEntry.ResponseType, + descriptorEntry.Descriptor); + logger.Debug( + $"Registered generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from assembly {assemblyName}."); } } diff --git a/GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs b/GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs new file mode 100644 index 00000000..2f40789f --- /dev/null +++ b/GFramework.Cqrs/Internal/SequentialNotificationPublisher.cs @@ -0,0 +1,35 @@ +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Notification; + +namespace GFramework.Cqrs.Internal; + +/// +/// 默认的通知发布器实现。 +/// +/// +/// 该实现完整保留当前 CQRS runtime 的既有通知语义:按已解析顺序逐个执行处理器, +/// 并在首个处理器抛出异常时立即停止后续发布。 +/// +internal sealed class SequentialNotificationPublisher : INotificationPublisher +{ + /// + /// 按既定顺序逐个执行当前通知的处理器。 + /// + /// 通知类型。 + /// 当前发布调用的执行上下文。 + /// 取消令牌。 + /// 表示通知发布完成的值任务。 + /// + public async ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + + foreach (var handler in context.Handlers) + { + await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/GFramework.Cqrs/Notification/DelegatingNotificationPublishContext.cs b/GFramework.Cqrs/Notification/DelegatingNotificationPublishContext.cs new file mode 100644 index 00000000..ee78517a --- /dev/null +++ b/GFramework.Cqrs/Notification/DelegatingNotificationPublishContext.cs @@ -0,0 +1,52 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Notification; + +/// +/// 通过内部回调桥接 dispatcher 执行逻辑的通知发布上下文。 +/// +/// 通知类型。 +/// 执行单个处理器所需的内部状态类型。 +internal sealed class DelegatingNotificationPublishContext : NotificationPublishContext + where TNotification : INotification +{ + private readonly NotificationHandlerExecutor _handlerExecutor; + private readonly TState _state; + + /// + /// 初始化一个委托驱动的通知发布上下文。 + /// + /// 当前通知。 + /// 当前发布调用已解析到的处理器集合。 + /// 执行处理器时需要的内部状态。 + /// 执行单个处理器时调用的内部回调。 + /// + /// 。 + /// + internal DelegatingNotificationPublishContext( + TNotification notification, + IReadOnlyList handlers, + TState state, + NotificationHandlerExecutor handlerExecutor) + : base(notification, handlers) + { + ArgumentNullException.ThrowIfNull(handlerExecutor); + + _state = state; + _handlerExecutor = handlerExecutor; + } + + /// + /// 通过默认 dispatcher 提供的内部回调执行单个处理器。 + /// + /// 要执行的处理器实例。 + /// 取消令牌。 + /// 表示当前处理器执行完成的值任务。 + /// + public override ValueTask InvokeHandlerAsync(object handler, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(handler); + + return _handlerExecutor(handler, Notification, _state, cancellationToken); + } +} diff --git a/GFramework.Cqrs/Notification/INotificationPublisher.cs b/GFramework.Cqrs/Notification/INotificationPublisher.cs new file mode 100644 index 00000000..67000367 --- /dev/null +++ b/GFramework.Cqrs/Notification/INotificationPublisher.cs @@ -0,0 +1,27 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Notification; + +/// +/// 定义默认 CQRS runtime 的通知发布策略。 +/// +/// +/// dispatcher 会先解析当前通知对应的处理器集合,再把本次发布上下文交给该抽象决定执行顺序。 +/// 实现应把 视为当前发布调用的瞬时数据, +/// 不要跨发布缓存处理器实例或假设它们已经脱离当前上下文。 +/// +public interface INotificationPublisher +{ + /// + /// 执行一次通知发布。 + /// + /// 通知类型。 + /// 当前发布调用的处理器集合与执行入口,不能为空。 + /// 取消令牌。 + /// 表示通知发布完成的值任务。 + /// + ValueTask PublishAsync( + NotificationPublishContext context, + CancellationToken cancellationToken = default) + where TNotification : INotification; +} diff --git a/GFramework.Cqrs/Notification/NotificationHandlerExecutor.cs b/GFramework.Cqrs/Notification/NotificationHandlerExecutor.cs new file mode 100644 index 00000000..9dcf28ee --- /dev/null +++ b/GFramework.Cqrs/Notification/NotificationHandlerExecutor.cs @@ -0,0 +1,20 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Notification; + +/// +/// 表示默认 dispatcher 执行单个通知处理器时使用的内部回调。 +/// +/// 通知类型。 +/// 执行当前处理器所需的内部状态类型。 +/// 要执行的处理器实例。 +/// 当前通知。 +/// 当前处理器执行所需的内部状态。 +/// 取消令牌。 +/// 表示当前处理器执行完成的值任务。 +internal delegate ValueTask NotificationHandlerExecutor( + object handler, + TNotification notification, + TState state, + CancellationToken cancellationToken) + where TNotification : INotification; diff --git a/GFramework.Cqrs/Notification/NotificationPublishContext.cs b/GFramework.Cqrs/Notification/NotificationPublishContext.cs new file mode 100644 index 00000000..0ddeda81 --- /dev/null +++ b/GFramework.Cqrs/Notification/NotificationPublishContext.cs @@ -0,0 +1,50 @@ +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Notification; + +/// +/// 表示一次通知发布调用的执行上下文。 +/// +/// 通知类型。 +/// +/// 该上下文把“当前通知”“已解析处理器集合”和“执行单个处理器”的入口收敛到同一对象中, +/// 使发布策略只需决定遍历、排序或并发方式,而无需了解 dispatcher 的上下文注入细节。 +/// +public abstract class NotificationPublishContext + where TNotification : INotification +{ + /// + /// 初始化一次通知发布上下文。 + /// + /// 当前通知。 + /// 当前发布调用已解析到的处理器集合。 + /// + /// 。 + /// + protected NotificationPublishContext(TNotification notification, IReadOnlyList handlers) + { + ArgumentNullException.ThrowIfNull(notification); + ArgumentNullException.ThrowIfNull(handlers); + + Notification = notification; + Handlers = handlers; + } + + /// + /// 获取当前要发布的通知。 + /// + public TNotification Notification { get; } + + /// + /// 获取当前发布调用已解析到的处理器集合。 + /// + public IReadOnlyList Handlers { get; } + + /// + /// 执行单个通知处理器。 + /// + /// 要执行的处理器实例。 + /// 取消令牌。 + /// 表示当前处理器执行完成的值任务。 + public abstract ValueTask InvokeHandlerAsync(object handler, CancellationToken cancellationToken); +} diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 62547a39..65a92db5 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -49,6 +49,7 @@ - 默认 runtime 与注册入口 - `CqrsRuntimeFactory.cs` - `Internal/CqrsDispatcher.cs` + - `Notification/INotificationPublisher.cs` - `Internal/CqrsHandlerRegistrar.cs` - `Internal/DefaultCqrsHandlerRegistrar.cs` - `Internal/DefaultCqrsRegistrationService.cs` @@ -122,6 +123,8 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,未找到处理器会抛出异常。 - 通知分发 - 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。 + - 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。 + - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器。 - 流式请求 - 通过 `IStreamRequest` 和 `IStreamRequestHandler<,>` 返回 `IAsyncEnumerable`。 - 上下文注入 diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 2b83cddc..9961a139 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1689,6 +1689,107 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string RequestInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsRequestInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsRequestInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsRequestInvokerDescriptor + { + public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsRequestInvokerDescriptorEntry + { + public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsRequestInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest(string Value) : IRequest; + + public sealed class VisibleHandler : IRequestHandler + { + public ValueTask Handle(VisibleRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(request.Value); + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -2150,8 +2251,6 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); - var generatedSource = execution.GeneratedSources[0].content; - Assert.Multiple(() => { Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); @@ -2159,6 +2258,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That(generatorErrors, Is.Empty); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + var generatedSource = execution.GeneratedSources[0].content; Assert.That( generatedSource, Does.Contain( @@ -2201,8 +2301,6 @@ public class CqrsHandlerRegistryGeneratorTests var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); - var generatedSource = execution.GeneratedSources[0].content; - Assert.Multiple(() => { Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306")); @@ -2210,6 +2308,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That(generatorErrors, Is.Empty); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + var generatedSource = execution.GeneratedSources[0].content; Assert.That( generatedSource, Does.Contain( @@ -2240,6 +2339,54 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 暴露 request invoker provider 契约时,生成器会让 generated registry 同时发射 + /// request invoker 描述符与对应的开放静态 invoker 方法。 + /// + [Test] + public void Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available() + { + var execution = ExecuteGenerator(RequestInvokerProviderSource); + var inputCompilationErrors = execution.InputCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.Multiple(() => + { + Assert.That(inputCompilationErrors, Is.Empty); + Assert.That(generatedCompilationErrors, Is.Empty); + Assert.That(generatorErrors, Is.Empty); + Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + var generatedSource = execution.GeneratedSources[0].content; + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsRequestInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.VisibleRequest), typeof(string),")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)")); + Assert.That( + generatedSource, + Does.Contain( + "private static global::System.Threading.Tasks.ValueTask InvokeRequestHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "public global::System.Collections.Generic.IReadOnlyList GetDescriptors()")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index 6109bd54..adcb83ea 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -7,6 +7,7 @@ using GFramework.Core.Ioc; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Command; +using GFramework.Cqrs.Notification; using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Tests.Common; @@ -51,6 +52,8 @@ public static class CqrsTestRuntime /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, /// 而无需完整启动服务模块管理器。 /// 该方法按服务类型执行幂等注册,只会补齐当前容器中尚未接线的 CQRS 基础设施。 + /// 若容器里只预注册了正式 seam,本方法也会补齐旧命名空间下的 + /// 兼容别名,并保持它与正式 seam 指向同一实例。 /// public static void RegisterInfrastructure(MicrosoftDiContainer container) { @@ -59,13 +62,14 @@ public static class CqrsTestRuntime if (container.Get() is null) { var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); - var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger); + var notificationPublisher = container.Get(); + var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher); container.Register(runtime); - container.Register((LegacyICqrsRuntime)runtime); + RegisterLegacyRuntimeAlias(container, runtime); } else if (container.Get() is null) { - container.Register((LegacyICqrsRuntime)container.GetRequired()); + RegisterLegacyRuntimeAlias(container, container.GetRequired()); } if (container.Get() is null) @@ -84,6 +88,25 @@ public static class CqrsTestRuntime } } + /// + /// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。 + /// + /// 承载运行时实例的测试容器。 + /// 当前正式 CQRS runtime 实例。 + /// + /// 测试辅助层显式保留这条 helper,避免“已存在正式 seam 时再补旧别名”的兼容语义分散在多个分支里。 + /// + private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime) + { + if (runtime is not LegacyICqrsRuntime legacyRuntime) + { + throw new InvalidOperationException( + $"The registered {nameof(ICqrsRuntime)} must also implement {typeof(LegacyICqrsRuntime).FullName}."); + } + + container.Register(legacyRuntime); + } + /// /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 /// diff --git a/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md new file mode 100644 index 00000000..88fdc64e --- /dev/null +++ b/ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md @@ -0,0 +1,98 @@ +# CQRS 与 Mediator 评估归档(RP-063) + +## 背景 + +- 本轮目的不是继续实现 runtime / generator,而是基于当前 `feat/cqrs-optimization` 工作树事实, + 评估 `GFramework.Cqrs` 相对 `ai-libs/Mediator` 的替代完成度、设计吸收度与后续可借鉴项。 +- 本评估只使用仓库内现有实现、文档、`ai-plan` 记录与只读第三方参考副本 `ai-libs/Mediator`。 + +## 评估结论 + +### 1. 当前阶段 + +- `cqrs-rewrite` 当前处于 `Phase 8`,恢复点提升到 `CQRS-REWRITE-RP-063`。 +- 当前主线已从“移除外部依赖并让默认 runtime 可用”转入“继续扩大 generator 覆盖、继续压低 dispatch / + invoker 反射占比、继续收口 facade 与兼容层”的中后期收敛阶段。 +- 这一结论延续了 active tracking 中的 `RP-062` 事实,不代表回退到早期迁移阶段。 + +### 2. 对外部 Mediator 的替代完成度 + +- 生产依赖层面:已基本完成替代。 + - `GFramework.Cqrs` 当前不再引用外部 `Mediator` 包。 + - 默认 runtime 已切换为自有 `CqrsDispatcher`、`CqrsHandlerRegistrar` 与 `CqrsRuntimeFactory`。 + - `GFramework.Core` 通过 `CqrsRuntimeModule` 自动接线默认 CQRS runtime。 +- 运行时主路径层面:已基本完成替代。 + - `ArchitectureContext` 已提供统一的 `SendRequestAsync`、`PublishAsync`、`CreateStream` 入口。 + - handler 注册链路已以 generated registry 优先、targeted fallback 次之、整程序集扫描兜底为主。 +- 结论: + - 若“完全替代”指“不再依赖外部 `Mediator` 作为生产 runtime”,答案是 `是`。 + - 若“完全替代”指“仓库内部所有旧总线、旧 seam、旧命名与兼容入口都已删除”,答案是 `否`。 + +### 3. 对 Mediator 设计思想的吸收度 + +- 已吸收: + - 统一 request / command / query / notification / stream 消息模型。 + - 源码生成优先、反射 fallback 次之的注册策略。 + - 通过缓存压低 registrar / dispatcher 热路径上的重复反射成本。 + - 将 CQRS runtime 明确接入框架架构上下文,而不是维持独立外部服务入口。 +- 部分吸收: + - `GFramework.Cqrs.SourceGenerators` 已深度参与 handler 注册,但当前仍主要生成 registry 与 fallback 元数据, + 而非像 `Mediator` 那样进一步生成 runtime 主体或更大范围的 DI glue。 +- 尚未充分吸收: + - notification publisher 作为可替换策略的一等抽象。 + - stream pipeline、pre/post processor、exception pipeline 这类更细粒度的扩展分层。 + - telemetry、diagnostics、benchmark、allocation tracking 作为框架主能力的完整体系。 + - 进一步把强类型 dispatch / invoker 主体前移到生成器,而不是继续依赖运行时 `MakeGenericMethod` + + `Delegate.CreateDelegate` 后再缓存。 + +### 4. 仓库内部仍未完全收口的部分 + +- 旧 `GFramework.Core.Command` / `GFramework.Core.Query` 路径仍然存在。 +- `ArchitectureContext` 仍同时暴露旧 Command / Query 路径与新 CQRS 路径。 +- `CqrsRuntimeModule` 仍注册 `LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 兼容别名。 +- `CqrsReflectionFallbackAttribute` 仍保留空 marker 与字符串 fallback 语义,说明 runtime 继续承担旧版兼容契约。 +- `GFramework.Cqrs.Tests/Mediator/` 目录与若干测试类仍沿用 `Mediator` 命名,但测试内部实际已调用当前 CQRS runtime。 +- 结论:当前真正残留的主要是兼容层、旧术语与评估/测试命名,而不是外部运行时依赖本身。 + +## 关键证据 + +- 当前阶段与主线: + - `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md` + - `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` +- 自有 runtime 与默认接线: + - `GFramework.Cqrs/CqrsRuntimeFactory.cs` + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` + - `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` + - `GFramework.Core/Services/Modules/CqrsRuntimeModule.cs` + - `GFramework.Core/Architectures/ArchitectureContext.cs` +- generator 与 fallback 合同: + - `GFramework.Cqrs.SourceGenerators/README.md` + - `GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs` + - `docs/zh-CN/core/cqrs.md` +- 第三方参考源: + - `ai-libs/Mediator/README.md` + - `ai-libs/Mediator/src/Mediator.SourceGenerator/**` + - `ai-libs/Mediator/src/Mediator/INotificationPublisher.cs` + - `ai-libs/Mediator/benchmarks/README.md` + +## 建议优先级 + +### P1 + +- 评估是否继续前移 generator 职责,生成部分强类型 dispatch / invoker 主体,而不再只停留在 registry。 +- 为 notification fan-out 引入可替换 publisher seam,并先定义顺序与并发两种语义模型。 + +### P2 + +- 扩展 pipeline 体系,评估 stream pipeline、pre-processor、post-processor、exception handler 的契约边界。 +- 为 CQRS runtime 设计 tracing / metrics seam,至少先完成 contracts 与默认 no-op / logger 对齐方案评估。 + +### P3 + +- 建立 CQRS runtime 的 benchmark / allocation 基线,让“继续压低反射成本”从经验判断转为可量化验证。 +- 单独规划旧 `Command` / `Query`、`LegacyICqrsRuntime` 与测试命名的收口顺序,不与 runtime 微优化混在同一波。 + +## 默认下一步 + +1. 以 `notification publisher seam` 与 `dispatch/invoker 生成前移` 为首轮设计评估对象。 +2. 若进入实现阶段,优先做 seam 与契约扩展,再决定是否调整旧测试目录与历史命名。 diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index 4703ad8c..64a394db 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -7,10 +7,48 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-062` +- 恢复点编号:`CQRS-REWRITE-RP-067` - 当前阶段:`Phase 8` - 当前焦点: + - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` + - 当前评估结论已明确:`GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,但仓库内部旧总线 API、 + 兼容 seam、fallback 旧语义与测试命名仍未完全收口 + - 当前评估结论已明确:相对 `ai-libs/Mediator`,框架已吸收统一消息模型、generator 优先注册与热路径缓存思路, + 但仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 + - 下一阶段建议优先级已收敛为:`notification publisher seam`、`dispatch/invoker 生成前移`、`pipeline 分层扩展`、 + `可观测性 seam` 与 `benchmark / allocation baseline` - 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口 + - 已完成一轮 notification publisher seam 最小落地:`GFramework.Cqrs` 新增 `INotificationPublisher`、 + `NotificationPublishContext` 与默认 `SequentialNotificationPublisher` + - `CqrsDispatcher` 现会在解析当前通知处理器集合后,把执行顺序委托给 publisher seam;默认行为仍保持 + “零处理器静默完成、顺序执行、首错即停” + - `CqrsRuntimeFactory`、`CqrsRuntimeModule` 与 `GFramework.Tests.Common.CqrsTestRuntime` 现支持在 runtime 创建前复用 + 容器里已显式注册的 `INotificationPublisher` + - 已补充 `CqrsNotificationPublisherTests`,覆盖自定义 publisher 接管、上下文注入、零处理器静默完成、首错即停,以及 + `RegisterInfrastructure` 默认接线复用预注册 publisher 的回归 + - 已完成一轮 `Mediator` 测试命名收口: + - `MediatorAdvancedFeaturesTests` -> `CqrsArchitectureContextAdvancedFeaturesTests` + - `MediatorArchitectureIntegrationTests` -> `CqrsArchitectureContextIntegrationTests` + - `MediatorComprehensiveTests` -> `ArchitectureContextComprehensiveTests` + - `GFramework.Cqrs.Tests` 中这三份历史测试现已统一迁入 `Cqrs/` 目录,并将命名空间、类名、中文注释与嵌套测试类型中的 + `Mediator` 语义收口为 `CQRS` / `ArchitectureContext` + - 已补充 `ArchitectureContextTests` 并发 lazy-resolution 回归,锁定 `PublishAsync(...)` 与 `CreateStream(...)` + 在并发首次访问时也只会解析一次 `ICqrsRuntime` + - 已完成一轮 `LegacyICqrsRuntime` compatibility slice 收口: + - `CqrsRuntimeModule` 与 `GFramework.Tests.Common.CqrsTestRuntime` 现把 legacy alias 注册收敛到显式 helper + - `MicrosoftDiContainerTests` 已补充“只预注册正式 `ICqrsRuntime` seam 时,也会回填 legacy alias 且保持同实例”的回归 + - `GFramework.Core.Abstractions/README.md`、`docs/zh-CN/abstractions/core-abstractions.md` 与 + `docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留, + 新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` + - 已完成一轮 `dispatch/invoker` 生成前移的最小 request 切片: + - `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、 + `CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry` + - generated registry 若实现 request invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器, + 并把 provider 枚举出的 request invoker 描述符写入 dispatcher 的进程级弱缓存 + - `CqrsDispatcher` 现会在首次创建 request dispatch binding 时优先命中 generated request invoker 描述符; + 未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径 + - `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义 + - `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 request invoker provider 成员与 invoker 方法 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 - 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据 @@ -41,6 +79,7 @@ CQRS 迁移与收敛。 - 已完成 `GFramework.Cqrs.Abstractions` / `GFramework.Cqrs` 项目骨架与 runtime seam 收敛 - 已完成 handler registry generator 的多轮收敛,当前合法 closed handler contract 已统一收敛到更窄的注册路径 - 已完成一轮公开入口文档与 source-generator 命名空间收口 +- 已完成一轮 `CQRS vs Mediator` 对照评估,确认当前主问题已从“是否能替代外部依赖”转为“框架内部收口与能力深化顺序” - 已接入 `$gframework-pr-review`,可直接抓取当前分支对应 PR 的 CodeRabbit 评论、checks 和测试结果 ## 当前活跃事实 @@ -84,10 +123,11 @@ CQRS 迁移与收敛。 - 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断 - 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档 - `2026-04-30` 已重新执行 `$gframework-pr-review`: - - 当前分支对应 `PR #304`,状态为 `OPEN` - - latest reviewed commit 当前剩余 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads,集中在测试脆弱断言、共享测试状态并发保护,以及 `CqrsDispatcher` 的缓存线程模型文档 - - 本地核对后,已确认这些评论仍对应当前代码;MegaLinter 继续只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,CTRF 汇总为 `2203/2203` passed - - 已在本地完成 follow-up:request pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释 + - 当前分支对应 `PR #305`,状态为 `OPEN` + - 当前抓取到 `9` 条 CodeRabbit open threads、`2` 条 Greptile open threads;远端 CTRF 汇总为 `2214/2214` passed,MegaLinter 仍只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音 + - 本地核对后,已确认以下评论仍然成立并已完成修正:`ArchitectureContextTests` 并发测试失败路径释放、`CqrsGeneratedRequestInvokerProviderTests` 的全局 logger provider 恢复与私有缓存断言解耦、`CqrsArchitectureContextIntegrationTests` 的真实上下文注入断言、`GeneratedRequestInvokerRequest` / `INotificationPublisher` XML 文档、`CqrsHandlerRegistrar` 的 provider 注册顺序、`CqrsTestRuntime` 的 legacy alias 显式失败模式,以及 `cqrs-rewrite` trace 重复标题 + - 对于 `ICqrsRequestInvokerProvider` / generated `TryGetDescriptor(...)` 相关 Greptile 评论,本地评估后未改 dispatcher 热路径语义;改为补齐公开注释与生成器方法级注释,明确默认 runtime 只在注册阶段经 `IEnumeratesCqrsRequestInvokerDescriptors` 预热缓存,`TryGetDescriptor(...)` 保留为显式查询 seam + - 本轮额外修正了 `GFramework.SourceGenerators.Tests` 中先读取 `GeneratedSources[0]` 再断言长度的脆弱顺序,并将 `ArchitectureContextTests` 的并发 orchestration 收敛到公共 helper,消除本轮引入的 `MA0051` warning - `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强: - `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归 - 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()` 链 @@ -129,20 +169,41 @@ CQRS 迁移与收敛。 - `SourceEmission` 不再保留 `MakePointerType()` 源码发射分支,`RuntimeTypeReferences` 也已删掉对应的外部程序集递归扫描死代码 - pointer / function pointer 的拒绝语义保持不变,direct / named / mixed fallback 逻辑未改动 - 当前工作区相对 `origin/main` 的累计 diff 已达到 `14 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition +- `2026-04-30` 已完成一轮 `CQRS vs Mediator` 结构化评估: + - 生产依赖与默认 runtime 接线层面,`GFramework.Cqrs` 已完成对外部 `Mediator` 的替代 + - 仓库内部收口层面,旧 `Command` / `Query` API、`LegacyICqrsRuntime` 别名、fallback 空 marker 兼容语义与 + `Mediator` 测试命名仍然存在 + - 设计吸收层面,当前已吸收统一消息模型、generator 优先注册与反射收敛思路;仍未完整吸收 publisher 策略抽象、 + stream / exception pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 + - 详细结论与证据已归档到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` +- `2026-04-30` 已接受两条只读 subagent 结论并完成 notification publisher seam 最小实现: + - 相对 `ai-libs/Mediator`,本轮只吸收 notification publisher 的策略接缝,不照搬 `NotificationHandlers` 包装、 + 并行 publisher 或异常聚合语义 + - 当前 seam 刻意保持在默认 runtime 内部:`ICqrsRuntime.PublishAsync(...)` 外形不变,dispatcher 仍负责 handler 解析与 + `IContextAware` 上下文注入 + - 用户若需替换通知发布策略,只需在 runtime 创建前向容器显式注册 `INotificationPublisher` +- `2026-04-30` 已接受三条 worker 切片并完成一轮测试命名收口: + - 三个 worker 分别独立拥有一份 `GFramework.Cqrs.Tests/Mediator/*.cs` 文件,主线程只做集成验证与后续追踪更新 + - 当前分支已不再保留 `GFramework.Cqrs.Tests/Mediator/` 目录下的生产内涵测试,相关文件均迁移到 `GFramework.Cqrs.Tests/Cqrs/` + - 本轮没有修改测试行为,只收口命名、注释、局部变量与嵌套测试类型语义 - 当前主线优先级: - - generator 覆盖面继续扩大 - - dispatch/invoker 反射占比继续下降 + - dispatch/invoker 反射占比继续下降,并优先评估生成前移方案 + - 基于已落地 publisher seam,继续评估是否需要公开配置面、并行策略或 telemetry decorator - package / facade / 兼容层继续收口 + - pipeline 分层扩展、可观测性 seam 与 benchmark baseline 进入中期候选 ## 当前风险 - 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 - 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖 +- 当前对外替代已基本完成,但若不单独规划旧 `Command` / `Query`、`LegacyICqrsRuntime` 与测试命名的收口顺序, + 后续仍会持续混淆“生产替代已完成”与“仓库内部收口未完成”这两个不同结论 ## 活跃文档 - 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md) - 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md) +- CQRS 与 Mediator 评估归档:[cqrs-vs-mediator-assessment-rp063.md](../archive/todos/cqrs-vs-mediator-assessment-rp063.md) - 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md) - `RP-046` 至 `RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md) @@ -152,14 +213,61 @@ CQRS 迁移与收敛。 - `RP-046` 至 `RP-062` 的历史验证命令与阶段性结果已移入验证归档,active tracking 只保留当前恢复入口需要的最新验证 - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json` - 结果:通过 - - 备注:确认当前分支对应 `PR #304`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread + - 备注:确认当前分支对应 `PR #305`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable`、`_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsArchitectureContextIntegrationTests.Handler_Can_Access_Architecture_Context|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Request_With_Retry_Behavior_Should_Succeed_On_First_Attempt|FullyQualifiedName~CqrsArchitectureContextAdvancedFeaturesTests.Transient_Error_Request_Should_Succeed_Without_Simulated_Errors"` + - 结果:通过 + - 备注:`5/5` passed;覆盖 generated invoker provider、真实上下文注入与两条重命名高级行为测试 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~PublishAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently|FullyQualifiedName~CreateStream_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently"` + - 结果:通过 + - 备注:`3/3` passed;确认并发首次解析测试在失败路径释放调整后保持通过 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~Emits_Direct_Type_Fallback_Metadata_When_All_Fallback_Handlers_Are_Referenceable_And_Runtime_Type_Contract_Is_Available|FullyQualifiedName~Emits_Mixed_Direct_Type_And_String_Fallback_Metadata_When_Runtime_Allows_Multiple_Fallback_Attributes"` + - 结果:通过 + - 备注:`3/3` passed;确认 provider 生成分支注释与断言顺序修正未改变生成语义 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:构建成功;并行验证期间出现过 `MSB3026` 拷贝重试噪音,属于同时运行多个 `dotnet` 命令时的输出文件竞争,不是持久性编译 warning - `bash scripts/validate-csharp-naming.sh` - 结果:通过 - 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;本轮确认 notification publisher seam、README 与文档更新未引入 `GFramework.Cqrs` 构建告警 +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsNotificationPublisherTests"` + - 结果:通过 + - 备注:`5/5` 通过;覆盖自定义 publisher 顺序、上下文注入、零处理器、首错即停与默认接线复用 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过 + - 备注:`41/41` 通过;确认 CQRS 基础设施默认接线与容器行为未回归 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过 + - 备注:`42/42` 通过;本轮新增 legacy alias 回填回归后,确认正式 seam 与旧命名空间 alias 仍指向同一实例 +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"` + - 结果:通过 + - 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过 + - 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员 +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警 +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"` + - 结果:通过 + - 备注:`22/22` 通过;新增 `PublishAsync` / `CreateStream` 并发首次访问只解析一次 `ICqrsRuntime` 的回归 ## 下一步 -1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项 +1. 基于已落地的 notification publisher seam,评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator +2. 基于已落地的 request invoker provider,评估是否继续把 notification / stream 的 invoker 也前移,或先补 provider 发现/诊断与文档入口 +3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级 diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md index 821683e2..68ce00ac 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -2,36 +2,169 @@ ## 2026-04-30 -### 阶段:PR #304 剩余 review follow-up 收敛(CQRS-REWRITE-RP-062) +### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067) -- 本轮再次执行 `$gframework-pr-review`,确认当前分支 `feat/cqrs-optimization` 仍对应 `PR #304` -- 本地复核后继续收敛了上一轮遗留的 review 项: - - `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已补 `NonParallelizable` - - `GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs` 已改用 `_syncRoot` 命名,并补齐缺失的 XML 文档标签 - - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 三个内部 `Handle(...)` 已补齐 XML `param` / `returns` - - `DispatcherNotificationContextRefreshNotification` 与 `DispatcherStreamContextRefreshRequest` 已补 `DispatchId` XML 参数注释 - - `cqrs-rewrite` active tracking / trace 已压缩为当前恢复入口,并将已完成阶段的详细历史移入 archive -- 验证: - - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` +- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` +- 在 `RP-066` 提交后复算 branch diff,相对 `origin/main` 增长到 `22 files`,仍明显低于 `50 files` stop condition,因此继续下一批 +- 本轮 critical path 保持在主线程,本地完成 `dispatch/invoker` 生成前移的最小 request 切片;尝试委派 source-generator 测试给 worker 时因 subagent 名额已满失败,因此主线程直接接管该测试修改 +- 本轮关键设计调整: + - 不按 `requestType.Assembly` 做 provider 发现,避免“请求定义在 A、handler 与 generated registry 在 B”时漏掉 generated invoker + - generated registry 若实现 `ICqrsRequestInvokerProvider`,registrar 会在激活 registry 后把 provider 注册进容器,并通过 `IEnumeratesCqrsRequestInvokerDescriptors` 把描述符写入 dispatcher 的进程级弱缓存 + - dispatcher 首次创建 request dispatch binding 时只按 `requestType + responseType` 读取静态弱缓存,不依赖具体容器实例;未命中时仍走既有反射创建路径 +- 已完成实现: + - `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、 + `CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry` + - `CqrsHandlerRegistrar` 现会识别 generated registry 的 request invoker provider 能力,并登记 provider 与 request invoker 描述符 + - `CqrsDispatcher` 新增 generated request invoker 弱缓存,并在 request binding 创建时优先消费该元数据 + - `CqrsHandlerRegistryGenerator` 在 runtime 合同可用时,会让 generated registry 额外实现 request invoker provider 相关接口,并发射 descriptor 列表、`TryGetDescriptor(...)`、`GetDescriptors()` 与 request invoker 静态方法 +- 已补充测试: + - `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider,且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果 + - `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法 + +### 验证(RP-067) + +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"` + - 结果:通过,`22/22` passed +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过,`1/1` passed +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + +### 当前下一步(RP-067) + +1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义 +2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files` + +### 阶段:LegacyICqrsRuntime compatibility slice 收口(CQRS-REWRITE-RP-066) + +- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` +- 在 `RP-065` 之后复算 branch diff,相对 `origin/main` 仍为 `19 files`,明显低于 `50 files` stop condition,因此继续下一批 +- 本轮按“关键路径本地、非冲突文档委派”的方式拆成两个切片: + - worker:`GFramework.Core.Abstractions/README.md`、`docs/zh-CN/abstractions/core-abstractions.md`、`docs/zh-CN/core/cqrs.md` + - 主线程:`GFramework.Core/Services/Modules/CqrsRuntimeModule.cs`、`GFramework.Tests.Common/CqrsTestRuntime.cs`、`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` +- 接受只读 subagent 结论后,将 `LegacyICqrsRuntime` 定位为“容器兼容层”,明确本轮不删除别名、不改 dispatcher 主体、不与旧 `Command` / `Query` API 清理混做 +- 主线程已完成: + - `CqrsRuntimeModule` 把 legacy alias 注册收敛到 `RegisterLegacyRuntimeAlias(...)` helper,并在 XML 文档里明确新旧服务类型解析到同一 runtime 实例 + - `CqrsTestRuntime.RegisterInfrastructure(...)` 现也通过同名 helper 补齐 legacy alias;当容器只预注册正式 `ICqrsRuntime` seam 时,会在幂等接线时回填旧命名空间 alias + - `MicrosoftDiContainerTests` 新增 `RegisterInfrastructure_Should_Backfill_Legacy_Cqrs_Runtime_Alias_With_The_Same_Instance`,锁定“只存在正式 seam 时也会补旧 alias,且两者仍指向同一实例”的兼容合同 +- worker 已完成文档收口: + - `GFramework.Core.Abstractions/README.md` + - `docs/zh-CN/abstractions/core-abstractions.md` + - `docs/zh-CN/core/cqrs.md` + - 三处文档都已明确:`GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 只是旧命名空间下保留的 compatibility alias,新代码应依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` + +### 验证(RP-066) + +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`42/42` passed +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + +### 当前下一步(RP-066) + +1. 在保持 branch diff 低于阈值的前提下,回到 `dispatch/invoker` 生成前移主线 +2. 优先尝试只覆盖 request 路径的 generated invoker/provider 最小切片,避免一次卷入 notification / stream / pipeline executor +3. 下一次 batch 结束后继续复算 branch diff,确认距 `50 files` stop condition 的剩余 headroom + +### 阶段:测试命名收口与 ArchitectureContext lazy-resolution 回归(CQRS-REWRITE-RP-065) + +- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main` +- `22f608eb` 之后复算 branch diff,相对 `origin/main` 已达到 `18 files`,仍明显低于 `50 files` stop condition,因此继续下一批 +- 本轮拆成四个互不冲突切片: + - worker 1:`MediatorAdvancedFeaturesTests.cs` + - worker 2:`MediatorArchitectureIntegrationTests.cs` + - worker 3:`MediatorComprehensiveTests.cs` + - 主线程:`GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` +- 三个 worker 均只收口单文件命名与注释语义,并把测试文件迁移到 `GFramework.Cqrs.Tests/Cqrs/` +- 主线程新增 `ArchitectureContextTests` 并发 lazy-resolution 回归,锁定: + - `PublishAsync(...)` 在并发首次访问时只解析一次 `ICqrsRuntime` + - `CreateStream(...)` 在并发首次访问时只解析一次 `ICqrsRuntime` +- 集成后已确认三份测试文件中不再残留 `GFramework.Cqrs.Tests.Mediator` 命名空间或 `Mediator` 语义命名 + +### 验证(RP-065) + +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureContextTests"` + - 结果:通过,`22/22` passed + +### 当前下一步(RP-065) + +1. 继续 `Phase 8` 主线,回到 `dispatch/invoker` 生成前移或 `LegacyICqrsRuntime` 收口的下一个低风险切片 +2. 在下一次 batch 结束后复算 branch diff,确认距 `50 files` stop condition 的剩余 headroom + +### 阶段:notification publisher seam 最小落地(CQRS-REWRITE-RP-064) + +- 本轮按 `gframework-batch-boot 50` 继续 `cqrs-rewrite`,基线使用本地现有 `origin/main` +- 当前 branch diff 相对 `origin/main` 开始时仅 `3 files / 164 lines`,远低于 `50 files` stop condition,因此继续推进真实代码切片 +- 主线程锁定 `notification publisher seam` 为本轮最低风险高收益切片,并保持关键路径在本地实现 +- 接受两条只读 subagent 结论: + - 对照 `ai-libs/Mediator` 后,只吸收 notification publisher 策略接缝,不在本轮引入并行 publisher、异常聚合或公开配置面 + - 现有仓库测试需要锁定的兼容语义是:零处理器静默完成、顺序执行、首错即停、上下文逐次注入 +- 已完成实现: + - `GFramework.Cqrs` 新增 `INotificationPublisher`、`NotificationPublishContext`、 + `DelegatingNotificationPublishContext` 与默认 `SequentialNotificationPublisher` + - `CqrsDispatcher.PublishAsync(...)` 改为解析 handlers 后构造发布上下文,并委托给 publisher seam 执行 + - `CqrsRuntimeFactory`、`CqrsRuntimeModule` 与 `GFramework.Tests.Common.CqrsTestRuntime` 现会在 runtime 创建前复用容器里已注册的 `INotificationPublisher` + - `GFramework.Cqrs.Tests` 新增 `CqrsNotificationPublisherTests`,覆盖自定义 publisher、上下文注入、零处理器、首错即停与默认接线复用 + - `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` 已同步说明默认通知语义与可替换 seam +- 中途验证曾因并行 .NET 构建产生输出文件锁噪音;已改为串行重跑并获取干净结果 + +### 验证(RP-064) + +- `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~CqrsNotificationPublisherTests"` + - 结果:通过,`5/5` passed +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`41/41` passed +- `GIT_DIR=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-cqrs GIT_WORK_TREE=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-cqrs bash scripts/validate-csharp-naming.sh` + - 结果:通过 + +### 当前下一步(RP-064) + +1. 评估 notification publisher seam 的第二阶段是否需要公开配置面、并行 publisher 或 telemetry decorator +2. 把 `dispatch/invoker` 生成前移重新拉回 `Phase 8` 主线,作为下一个实现切片 + +### 阶段:CQRS vs Mediator 评估归档(CQRS-REWRITE-RP-063) + +- 本轮按用户要求使用 `gframework-boot` 启动上下文后,先完成 `cqrs-rewrite` 现状核对,再并行对照 + `GFramework.Cqrs` 与 `ai-libs/Mediator` +- 只读评估结论已归档到 `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md` +- 本轮关键判断: + - `GFramework.Cqrs` 已完成对外部 `Mediator` 作为生产 runtime 依赖的替代 + - 当前尚未完成的是仓库内部旧 `Command` / `Query` API、兼容 seam、fallback 旧语义与测试命名的收口 + - 当前已吸收 `Mediator` 的统一消息模型、generator 优先注册与热路径缓存思路 + - 当前仍未完整吸收 publisher 策略抽象、细粒度 pipeline、telemetry / diagnostics / benchmark 体系与 runtime 主体生成 +- 本轮把默认下一步从“继续盯 PR thread”调整为“围绕 publisher seam 与 dispatch/invoker 生成前移做下一轮设计收敛” + +### 验证(RP-063) + +- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` - 结果:通过,`0 warning / 0 error` ## 活跃事实 - 当前主题仍处于 `Phase 8` -- `PR #304` 的本地 follow-up 已再次收口一轮,后续需要在 push 后重新观察 GitHub 的 unresolved thread 刷新结果 +- 当前主题的主问题已从“是否完成外部依赖替代”转为“内部兼容层收口顺序与下一轮能力深化优先级” - 已完成阶段的详细执行历史不再留在 active trace;默认恢复入口只保留当前恢复点、活跃事实、风险与下一步 ## 当前风险 - 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响 -- 远端 review thread 在本地提交前不会自动刷新,GitHub 上看到的 open 状态可能暂时滞后于当前代码 +- 若不把“生产替代完成”与“仓库内部收口完成”分开记录,后续很容易重复争论当前 CQRS 迁移是否已经完成 ## Archive Context +- 当前评估归档: + - `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-vs-mediator-assessment-rp063.md` - 历史 trace 归档: - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-through-rp043.md` - `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md` ## 当前下一步 -1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项 +1. 补一轮最小 Release 构建验证,确认本次 `ai-plan` 与评估文档更新未引入仓库级异常 +2. 以 `notification publisher seam` 与 `dispatch/invoker` 生成前移为优先对象,形成下一轮可执行设计 diff --git a/docs/zh-CN/abstractions/core-abstractions.md b/docs/zh-CN/abstractions/core-abstractions.md index db399a46..82830b46 100644 --- a/docs/zh-CN/abstractions/core-abstractions.md +++ b/docs/zh-CN/abstractions/core-abstractions.md @@ -75,7 +75,10 @@ public sealed class DiagnosticsFeature - 架构与模块入口:`IArchitecture`、`IArchitectureContext`、`IServiceModule` - 运行时基础设施:`IIocContainer`、`ILogger`、`IResourceManager`、`IConfigurationManager` - 状态与并发能力:`IStateMachine`、`IStore`、`IAsyncKeyLockManager`、`ITimeProvider` -- 迁移与组合边界:`ICommandExecutor`、`IQueryExecutor`、`ICqrsRuntime` +- 迁移与组合边界:`ICommandExecutor`、`IQueryExecutor`,以及旧命名空间下作为 compatibility alias 暴露的 `ICqrsRuntime` + +`GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 当前主要承担旧命名空间兼容入口的角色。编写新模块或新增请求处理逻辑时, +应直接引用 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`,让 runtime seam 与 CQRS 请求契约保持一致。 ## 契约族阅读入口 @@ -85,7 +88,7 @@ public sealed class DiagnosticsFeature | --- | --- | --- | | `Architectures/` | `IArchitecture`、`IArchitectureContext`、`IArchitectureServices`、`IServiceModule` | 架构上下文、服务访问面与模块安装 / 生命周期约束 | | `Lifecycle/` `Registries/` | `ILifecycle`、`IAsyncInitializable`、`IRegistry`、`KeyValueRegistryBase` | 初始化 / 销毁阶段和注册表抽象边界 | -| `Command/` `Query/` `Cqrs/` | `ICommandExecutor`、`IAsyncCommand`、`IQueryExecutor`、`ICqrsRuntime` | 旧命令 / 查询接口与新请求模型之间的兼容和迁移边界 | +| `Command/` `Query/` `Cqrs/` | `ICommandExecutor`、`IAsyncCommand`、`IQueryExecutor`、`ICqrsRuntime` | 旧命令 / 查询接口,以及 CQRS runtime compatibility alias 的迁移边界 | | `Events/` `Property/` | `IEventBus`、`IEventFilter`、`IBindableProperty`、`IReadonlyBindableProperty` | 事件传播、过滤、解绑对象和属性订阅语义 | | `State/` `StateManagement/` | `IStateMachine`、`IAsyncState`、`IStore`、`IStoreMiddleware` | 状态机契约与 Store 的 reducer / middleware / diagnostics 边界 | | `Coroutine/` `Time/` `Pause/` `Concurrency/` | `IYieldInstruction`、`ICoroutineStatistics`、`ITimeProvider`、`IPauseStackManager`、`IAsyncKeyLockManager` | 调度模型、时间源、暂停栈和异步锁契约 | diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 655f0b66..1fd1f601 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -13,12 +13,15 @@ description: Cqrs 模块族的运行时、契约层、生成器入口,以及 如果你在写新功能,优先使用这套请求模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。 +如果你在查找 `ICqrsRuntime`,请把 `GFramework.Core.Abstractions.Cqrs.ICqrsRuntime` 理解为旧命名空间下保留的 +legacy compatibility alias。新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`。 + ## 模块族边界 | 模块 | 角色 | 何时安装 | | --- | --- | --- | | `GeWuYou.GFramework.Cqrs.Abstractions` | 纯契约层,定义 request、notification、stream、handler、pipeline、runtime seam | 需要把消息契约放到更稳定的共享层,或只依赖接口做解耦 | -| `GeWuYou.GFramework.Cqrs` | 默认 runtime,提供 dispatcher、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 | +| `GeWuYou.GFramework.Cqrs` | 默认 runtime,提供 dispatcher、notification publisher seam、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 | | `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,让运行时先走生成注册器,再只对剩余 handler 做定向 fallback | handler 较多,想把注册映射前移到编译期 | ## 最小接入路径 @@ -109,6 +112,13 @@ var playerId = await architecture.Context.SendRequestAsync( 新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。 +当前通知分发默认仍保持顺序语义: + +- 零处理器时静默完成 +- 已解析处理器按容器顺序逐个执行 +- 首个处理器抛出异常时立即停止后续分发 +- 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置顺序发布器 + ## Request 与流式变体 除了最常见的 `Command` / `Query` / `Notification`,当前公开面还覆盖两类容易被忽略的入口: @@ -195,6 +205,12 @@ RegisterCqrsPipelineBehavior>(); `IArchitectureContext` 仍然兼容旧入口,但新代码应优先使用 CQRS runtime。 +这里有两个边界需要分开理解: + +- 旧 `Command` / `Query` 入口仍可用于维护历史调用链 +- 旧命名空间下的 `ICqrsRuntime` 只是为了兼容既有引用而保留的 alias;面向新代码时,应直接使用 + `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime` + 一个简单判断规则: - 在维护历史代码:允许继续使用旧 Command / Query