From 48e57c85476a9df095c5af3c2c9d6705685fc105 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:46:59 +0800 Subject: [PATCH 01/70] Replace Mediator runtime with built-in CQRS --- AGENTS.md | 10 + .../Architectures/IArchitectureContext.cs | 52 ++-- .../Cqrs/Command/ICommand.cs | 25 ++ .../Cqrs/INotification.cs | 9 + .../Cqrs/INotificationHandler.cs | 17 ++ .../Cqrs/IPipelineBehavior.cs | 22 ++ GFramework.Core.Abstractions/Cqrs/IRequest.cs | 10 + .../Cqrs/IRequestHandler.cs | 18 ++ .../Cqrs/IStreamRequest.cs | 10 + .../Cqrs/IStreamRequestHandler.cs | 18 ++ .../Cqrs/MessageHandlerDelegate.cs | 14 + .../Cqrs/Query/IQuery.cs | 18 ++ GFramework.Core.Abstractions/Cqrs/Unit.cs | 13 + .../ArchitectureModulesBehaviorTests.cs | 32 +-- .../ArchitectureServicesTests.cs | 25 +- .../Architectures/GameContextTests.cs | 25 +- GFramework.Core.Tests/CqrsTestRuntime.cs | 73 +++++ .../Mediator/MediatorAdvancedFeaturesTests.cs | 15 +- .../MediatorArchitectureIntegrationTests.cs | 15 +- .../Mediator/MediatorComprehensiveTests.cs | 47 ++-- .../Architectures/ArchitectureBootstrapper.cs | 11 +- .../Architectures/ArchitectureContext.cs | 94 +++---- .../Command/AbstractCommandWithInput.cs | 6 +- .../Command/AbstractCommandWithResult.cs | 6 +- .../Cqrs/Behaviors/LoggingBehavior.cs | 4 +- .../Cqrs/Behaviors/PerformanceBehavior.cs | 4 +- .../Cqrs/Command/AbstractCommandHandler.cs | 9 +- .../Command/AbstractStreamCommandHandler.cs | 7 +- GFramework.Core/Cqrs/Command/CommandBase.cs | 3 +- .../Cqrs/Internal/CqrsDispatcher.cs | 263 ++++++++++++++++++ .../Cqrs/Internal/CqrsHandlerRegistrar.cs | 81 ++++++ .../AbstractNotificationHandler.cs | 4 +- .../Cqrs/Notification/NotificationBase.cs | 4 +- .../Cqrs/Query/AbstractQueryHandler.cs | 7 +- .../Cqrs/Query/AbstractStreamQueryHandler.cs | 7 +- GFramework.Core/Cqrs/Query/QueryBase.cs | 3 +- .../Cqrs/Request/AbstractRequestHandler.cs | 6 +- .../Request/AbstractStreamRequestHandler.cs | 4 +- GFramework.Core/Cqrs/Request/RequestBase.cs | 4 +- .../ContextAwareMediatorCommandExtensions.cs | 11 +- .../ContextAwareMediatorExtensions.cs | 6 +- .../ContextAwareMediatorQueryExtensions.cs | 11 +- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 4 +- .../Query/AbstractQueryWithResult.cs | 4 +- ....GFramework.Godot.SourceGenerators.targets | 14 +- .../ContextAwareCoroutineExtensions.cs | 8 +- .../todos/cqrs-rewrite-migration-tracking.md | 109 ++++++++ .../traces/cqrs-rewrite-migration-trace.md | 68 +++++ 48 files changed, 993 insertions(+), 237 deletions(-) create mode 100644 GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/INotification.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/IRequest.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/Unit.cs create mode 100644 GFramework.Core.Tests/CqrsTestRuntime.cs create mode 100644 GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs create mode 100644 GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs create mode 100644 local-plan/todos/cqrs-rewrite-migration-tracking.md create mode 100644 local-plan/traces/cqrs-rewrite-migration-trace.md diff --git a/AGENTS.md b/AGENTS.md index c63a2bfa..720b53af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -244,6 +244,16 @@ bash scripts/validate-csharp-naming.sh - Tracking updates MUST reflect completed work, newly discovered issues, validation results, and the next recommended recovery point. - Completing code changes without updating the active tracking document is considered incomplete work. +- For any multi-step refactor, migration, or cross-module task, contributors MUST create or adopt a dedicated recovery + document under `local-plan/todos/` before making substantive code changes. +- Recovery documents MUST record the current phase, the active recovery point identifier, known risks, and the next + recommended resume step so another contributor or subagent can continue the work safely. +- Contributors MUST maintain a matching execution trace under `local-plan/traces/` for complex work. The trace should + record the current date, key decisions, validation milestones, and the immediate next step. +- When a task spans multiple commits or is likely to exceed a single agent context window, update both the recovery + document and the trace at each meaningful milestone before pausing or handing work off. +- If subagents are used on a complex task, the main agent MUST capture the delegated scope and any accepted findings in + the active recovery document or trace before continuing implementation. ### Repository Documentation diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index 3469d8db..9a08dec6 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -1,11 +1,13 @@ using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using Mediator; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Abstractions.Architectures; @@ -118,12 +120,12 @@ public interface IArchitectureContext TResult SendCommand(Command.ICommand command); /// - /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// 发送一个 CQRS 命令并返回结果。 /// - /// 命令响应类型 - /// 要发送的命令对象 - /// 命令执行结果 - TResponse SendCommand(Mediator.ICommand command); + /// 命令响应类型。 + /// 要发送的 CQRS 命令。 + /// 命令执行结果。 + TResponse SendCommand(GFramework.Core.Abstractions.Cqrs.Command.ICommand command); /// @@ -133,14 +135,13 @@ public interface IArchitectureContext Task SendCommandAsync(IAsyncCommand command); /// - /// [Mediator] 异步发送命令并返回结果 - /// 通过Mediator模式发送命令请求,支持取消操作 + /// 异步发送一个 CQRS 命令并返回结果。 /// - /// 命令响应类型 - /// 要发送的命令对象 - /// 取消令牌,用于取消操作 - /// 包含命令执行结果的ValueTask - ValueTask SendCommandAsync(Mediator.ICommand command, + /// 命令响应类型。 + /// 要发送的 CQRS 命令。 + /// 取消令牌。 + /// 包含命令执行结果的值任务。 + ValueTask SendCommandAsync(GFramework.Core.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default); @@ -161,12 +162,12 @@ public interface IArchitectureContext TResult SendQuery(Query.IQuery query); /// - /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// 发送一个 CQRS 查询并返回结果。 /// - /// 查询响应类型 - /// 要发送的查询对象 - /// 查询结果 - TResponse SendQuery(Mediator.IQuery query); + /// 查询响应类型。 + /// 要发送的 CQRS 查询。 + /// 查询结果。 + TResponse SendQuery(GFramework.Core.Abstractions.Cqrs.Query.IQuery query); /// /// 异步发送一个查询请求 @@ -177,14 +178,13 @@ public interface IArchitectureContext Task SendQueryAsync(IAsyncQuery query); /// - /// [Mediator] 异步发送查询并返回结果 - /// 通过Mediator模式发送查询请求,支持取消操作 + /// 异步发送一个 CQRS 查询并返回结果。 /// - /// 查询响应类型 - /// 要发送的查询对象 - /// 取消令牌,用于取消操作 - /// 包含查询结果的ValueTask - ValueTask SendQueryAsync(Mediator.IQuery query, + /// 查询响应类型。 + /// 要发送的 CQRS 查询。 + /// 取消令牌。 + /// 包含查询结果的值任务。 + ValueTask SendQueryAsync(GFramework.Core.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default); /// @@ -265,4 +265,4 @@ public interface IArchitectureContext /// /// 环境对象实例 IEnvironment GetEnvironment(); -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs new file mode 100644 index 00000000..ba04331e --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs @@ -0,0 +1,25 @@ +namespace GFramework.Core.Abstractions.Cqrs.Command; + +/// +/// 表示一个 CQRS 命令。 +/// 命令通常用于修改系统状态。 +/// +/// 命令响应类型。 +public interface ICommand : IRequest +{ +} + +/// +/// 表示一个无显式返回值的 CQRS 命令。 +/// +public interface ICommand : ICommand +{ +} + +/// +/// 表示一个流式 CQRS 命令。 +/// +/// 流式响应元素类型。 +public interface IStreamCommand : IStreamRequest +{ +} diff --git a/GFramework.Core.Abstractions/Cqrs/INotification.cs b/GFramework.Core.Abstractions/Cqrs/INotification.cs new file mode 100644 index 00000000..9d69e28e --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/INotification.cs @@ -0,0 +1,9 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示一个一对多发布的通知消息。 +/// 通知不要求返回值,允许被零个或多个处理器消费。 +/// +public interface INotification +{ +} diff --git a/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs b/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs new file mode 100644 index 00000000..23861d1d --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs @@ -0,0 +1,17 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示处理通知消息的处理器契约。 +/// +/// 通知类型。 +public interface INotificationHandler + where TNotification : INotification +{ + /// + /// 处理通知消息。 + /// + /// 要处理的通知。 + /// 取消令牌。 + /// 异步处理任务。 + ValueTask Handle(TNotification notification, CancellationToken cancellationToken); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs b/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs new file mode 100644 index 00000000..cd01aad3 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs @@ -0,0 +1,22 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义 CQRS 请求处理前后的管道行为。 +/// +/// 请求类型。 +/// 响应类型。 +public interface IPipelineBehavior + where TRequest : IRequest +{ + /// + /// 处理当前请求,并决定是否继续调用后续行为或最终处理器。 + /// + /// 当前请求消息。 + /// 下一个处理委托。 + /// 取消令牌。 + /// 请求响应。 + ValueTask Handle( + TRequest message, + MessageHandlerDelegate next, + CancellationToken cancellationToken); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IRequest.cs b/GFramework.Core.Abstractions/Cqrs/IRequest.cs new file mode 100644 index 00000000..26259fc4 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/IRequest.cs @@ -0,0 +1,10 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示一个有响应的 CQRS 请求。 +/// 该接口是命令、查询以及其他请求语义的统一基接口。 +/// +/// 请求响应类型。 +public interface IRequest +{ +} diff --git a/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs b/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs new file mode 100644 index 00000000..2415e282 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs @@ -0,0 +1,18 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示处理单个 CQRS 请求的处理器契约。 +/// +/// 请求类型。 +/// 响应类型。 +public interface IRequestHandler + where TRequest : IRequest +{ + /// + /// 处理指定请求并返回结果。 + /// + /// 要处理的请求。 + /// 取消令牌。 + /// 请求结果。 + ValueTask Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs new file mode 100644 index 00000000..05ffa5df --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs @@ -0,0 +1,10 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示一个流式 CQRS 请求。 +/// 请求处理器可以逐步产生响应序列,而不是一次性返回完整结果。 +/// +/// 流式响应元素类型。 +public interface IStreamRequest +{ +} diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs b/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs new file mode 100644 index 00000000..1c6e02a7 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs @@ -0,0 +1,18 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示处理流式 CQRS 请求的处理器契约。 +/// +/// 流式请求类型。 +/// 流式响应元素类型。 +public interface IStreamRequestHandler + where TRequest : IStreamRequest +{ + /// + /// 处理流式请求并返回异步响应序列。 + /// + /// 要处理的请求。 + /// 取消令牌。 + /// 异步响应序列。 + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs new file mode 100644 index 00000000..172f7f3b --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs @@ -0,0 +1,14 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示 CQRS 请求在管道中继续向下执行的处理委托。 +/// +/// 请求类型。 +/// 响应类型。 +/// 当前请求消息。 +/// 取消令牌。 +/// 请求响应。 +public delegate ValueTask MessageHandlerDelegate( + TRequest message, + CancellationToken cancellationToken) + where TRequest : IRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs new file mode 100644 index 00000000..cbb1586e --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs @@ -0,0 +1,18 @@ +namespace GFramework.Core.Abstractions.Cqrs.Query; + +/// +/// 表示一个 CQRS 查询。 +/// 查询用于读取数据,不应产生副作用。 +/// +/// 查询响应类型。 +public interface IQuery : IRequest +{ +} + +/// +/// 表示一个流式 CQRS 查询。 +/// +/// 流式响应元素类型。 +public interface IStreamQuery : IStreamRequest +{ +} diff --git a/GFramework.Core.Abstractions/Cqrs/Unit.cs b/GFramework.Core.Abstractions/Cqrs/Unit.cs new file mode 100644 index 00000000..7dc3da14 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/Unit.cs @@ -0,0 +1,13 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 表示没有实际返回值的 CQRS 响应类型。 +/// 该类型用于统一命令与请求的泛型签名,避免引入外部库的 Unit 定义。 +/// +public readonly record struct Unit +{ + /// + /// 获取默认的空响应实例。 + /// + public static Unit Value { get; } = new(); +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index 05601230..7485d978 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -2,14 +2,14 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using Mediator; -using Microsoft.Extensions.DependencyInjection; +using GFramework.Core.Tests; +using GfCqrs = GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; /// -/// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 Mediator 行为注册能力。 -/// 这些测试覆盖模块安装回调和中介管道行为接入,确保模块管理器仍然保持可观察行为不变。 +/// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 CQRS 行为注册能力。 +/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。 /// [TestFixture] public class ArchitectureModulesBehaviorTests @@ -57,7 +57,7 @@ public class ArchitectureModulesBehaviorTests } /// - /// 验证注册的 Mediator 行为会参与请求管道执行。 + /// 验证注册的 CQRS 行为会参与请求管道执行。 /// [Test] public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request() @@ -67,7 +67,9 @@ public class ArchitectureModulesBehaviorTests await architecture.InitializeAsync(); - var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); + var response = await CqrsTestRuntime.ExecutePipelineAsync( + architecture.Context, + new ModuleBehaviorRequest()); Assert.Multiple(() => { @@ -83,12 +85,6 @@ public class ArchitectureModulesBehaviorTests /// private sealed class ModuleTestArchitecture(Action registrationAction) : Architecture { - /// - /// 打开 Mediator 服务注册,以便测试中介行为接入。 - /// - public override Action? Configurator => - services => services.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; }); - /// /// 在初始化阶段执行测试注入的模块注册逻辑。 /// @@ -136,14 +132,14 @@ public class ArchitectureModulesBehaviorTests /// /// 用于验证管道行为注册是否生效的测试请求。 /// -public sealed class ModuleBehaviorRequest : IRequest +public sealed class ModuleBehaviorRequest : GfCqrs.IRequest { } /// /// 处理测试请求的处理器。 /// -public sealed class ModuleBehaviorRequestHandler : IRequestHandler +public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler { /// /// 返回固定结果,便于聚焦验证管道行为是否执行。 @@ -162,8 +158,8 @@ public sealed class ModuleBehaviorRequestHandler : IRequestHandler /// 请求类型。 /// 响应类型。 -public sealed class TrackingPipelineBehavior : IPipelineBehavior - where TRequest : IRequest +public sealed class TrackingPipelineBehavior : GfCqrs.IPipelineBehavior + where TRequest : GfCqrs.IRequest { /// /// 获取当前测试进程中该请求类型对应的行为触发次数。 @@ -179,10 +175,10 @@ public sealed class TrackingPipelineBehavior : IPipelineBeh /// 下游处理器的响应结果。 public async ValueTask Handle( TRequest message, - MessageHandlerDelegate next, + GfCqrs.MessageHandlerDelegate next, CancellationToken cancellationToken) { InvocationCount++; return await next(message, cancellationToken); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs index 93244298..512e8a55 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs @@ -347,58 +347,61 @@ public class TestArchitectureContextV3 : IArchitectureContext { } - public ValueTask SendRequestAsync(IRequest request, + public ValueTask SendRequestAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendRequest(IRequest request) + public TResponse SendRequest(global::GFramework.Core.Abstractions.Cqrs.IRequest request) { throw new NotImplementedException(); } - public ValueTask SendCommandAsync(global::Mediator.ICommand command, + public ValueTask SendCommandAsync( + global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(global::Mediator.ICommand command) + public TResponse SendCommand(global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync(global::Mediator.IQuery query, + public ValueTask SendQueryAsync( + global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(global::Mediator.IQuery query) + public TResponse SendQuery(global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } public ValueTask PublishAsync(TNotification notification, - CancellationToken cancellationToken = default) where TNotification : INotification + CancellationToken cancellationToken = default) where TNotification : global::GFramework.Core.Abstractions.Cqrs.INotification { throw new NotImplementedException(); } - public IAsyncEnumerable CreateStream(IStreamRequest request, + public IAsyncEnumerable CreateStream( + global::GFramework.Core.Abstractions.Cqrs.IStreamRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : IRequest + where TCommand : global::GFramework.Core.Abstractions.Cqrs.IRequest { throw new NotImplementedException(); } - public ValueTask SendAsync(IRequest command, + public ValueTask SendAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -439,4 +442,4 @@ public class TestArchitectureContextV3 : IArchitectureContext } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index 8f1bea57..6c2670bc 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -394,58 +394,61 @@ public class TestArchitectureContext : IArchitectureContext { } - public ValueTask SendRequestAsync(IRequest request, + public ValueTask SendRequestAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendRequest(IRequest request) + public TResponse SendRequest(global::GFramework.Core.Abstractions.Cqrs.IRequest request) { throw new NotImplementedException(); } - public ValueTask SendCommandAsync(global::Mediator.ICommand command, + public ValueTask SendCommandAsync( + global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(global::Mediator.ICommand command) + public TResponse SendCommand(global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync(global::Mediator.IQuery query, + public ValueTask SendQueryAsync( + global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(global::Mediator.IQuery query) + public TResponse SendQuery(global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } public ValueTask PublishAsync(TNotification notification, - CancellationToken cancellationToken = default) where TNotification : INotification + CancellationToken cancellationToken = default) where TNotification : global::GFramework.Core.Abstractions.Cqrs.INotification { throw new NotImplementedException(); } - public IAsyncEnumerable CreateStream(IStreamRequest request, + public IAsyncEnumerable CreateStream( + global::GFramework.Core.Abstractions.Cqrs.IStreamRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : IRequest + where TCommand : global::GFramework.Core.Abstractions.Cqrs.IRequest { throw new NotImplementedException(); } - public ValueTask SendAsync(IRequest command, + public ValueTask SendAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -510,4 +513,4 @@ public class TestArchitectureContext : IArchitectureContext { return Environment; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs new file mode 100644 index 00000000..ae1d3361 --- /dev/null +++ b/GFramework.Core.Tests/CqrsTestRuntime.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Architectures; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GfCqrs = GFramework.Core.Abstractions.Cqrs; + +namespace GFramework.Core.Tests; + +internal static class CqrsTestRuntime +{ + private static readonly MethodInfo RegisterHandlersMethod = typeof(ArchitectureContext).Assembly + .GetType("GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)! + .GetMethod( + "RegisterHandlers", + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)! + ?? throw new InvalidOperationException("Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + + public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(assemblies); + + var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + RegisterHandlersMethod.Invoke( + null, + [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]); + } + + public static ValueTask ExecutePipelineAsync( + IArchitectureContext context, + TRequest request, + CancellationToken cancellationToken = default) + where TRequest : class, GfCqrs.IRequest + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + var handlers = context.GetServices>(); + if (handlers.Count == 0) + throw new InvalidOperationException( + $"No CQRS request handler registered for {typeof(TRequest).FullName}."); + + if (handlers.Count > 1) + throw new InvalidOperationException( + $"Expected a single CQRS request handler for {typeof(TRequest).FullName}, but found {handlers.Count}."); + + var handler = handlers[0]; + PrepareContext(handler, context); + + GfCqrs.MessageHandlerDelegate pipeline = handler.Handle; + + var behaviors = context.GetServices>(); + for (var index = behaviors.Count - 1; index >= 0; index--) + { + var behavior = behaviors[index]; + PrepareContext(behavior, context); + + var next = pipeline; + pipeline = (message, token) => behavior.Handle(message, next, token); + } + + return pipeline(request, cancellationToken); + } + + private static void PrepareContext(object instance, IArchitectureContext context) + { + if (instance is IContextAware contextAware) + contextAware.SetContext(context); + } +} diff --git a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index bc5cd782..d3bf4041 100644 --- a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using Mediator; -using Microsoft.Extensions.DependencyInjection; +using GFramework.Core.Tests; namespace GFramework.Core.Tests.Mediator; @@ -26,11 +26,10 @@ public class MediatorAdvancedFeaturesTests loggerField?.SetValue(_container, LoggerFactoryResolver.Provider.CreateLogger(nameof(MediatorAdvancedFeaturesTests))); - // 注册Mediator及相关处理器 - _container.ExecuteServicesHook(configurator => - { - configurator.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; }); - }); + CqrsTestRuntime.RegisterHandlers( + _container, + typeof(MediatorAdvancedFeaturesTests).Assembly, + typeof(ArchitectureContext).Assembly); _container.Freeze(); _context = new ArchitectureContext(_container); @@ -487,4 +486,4 @@ public sealed record TestDatabaseRequest : IRequest public List Storage { get; init; } = new(); } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index 85bbf3aa..056517f7 100644 --- a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,12 +1,12 @@ using System.Diagnostics; using System.Reflection; using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using Mediator; -using Microsoft.Extensions.DependencyInjection; +using GFramework.Core.Tests; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Mediator; @@ -33,11 +33,10 @@ public class MediatorArchitectureIntegrationTests _commandBus = new CommandExecutor(); _container.RegisterPlurality(_commandBus); - // 注册Mediator - _container.ExecuteServicesHook(configurator => - { - configurator.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; }); - }); + CqrsTestRuntime.RegisterHandlers( + _container, + typeof(MediatorArchitectureIntegrationTests).Assembly, + typeof(ArchitectureContext).Assembly); _container.Freeze(); _context = new ArchitectureContext(_container); @@ -559,4 +558,4 @@ public class TestTraditionalCommand : ICommand public IArchitectureContext GetContext() => null!; } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs index 020bc26b..27522c16 100644 --- a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Events; using GFramework.Core.Architectures; using GFramework.Core.Command; @@ -10,13 +11,9 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; -using Mediator; -using Microsoft.Extensions.DependencyInjection; +using GFramework.Core.Tests; using ICommand = GFramework.Core.Abstractions.Command.ICommand; - -// ✅ Mediator 库的命名空间 - -// ✅ 使用 global using 或别名来区分 +using Unit = GFramework.Core.Abstractions.Cqrs.Unit; namespace GFramework.Core.Tests.Mediator; @@ -25,7 +22,7 @@ public class MediatorComprehensiveTests { /// /// 测试初始化方法,在每个测试方法执行前运行。 - /// 负责初始化日志工厂、依赖注入容器、Mediator以及各种总线服务。 + /// 负责初始化日志工厂、依赖注入容器、自有 CQRS 处理器以及各种总线服务。 /// [SetUp] public void SetUp() @@ -51,13 +48,11 @@ public class MediatorComprehensiveTests _container.RegisterPlurality(_asyncQueryBus); _container.RegisterPlurality(_environment); - // ✅ 注册 Mediator - _container.ExecuteServicesHook(configurator => - { - configurator.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; }); - }); + CqrsTestRuntime.RegisterHandlers( + _container, + typeof(MediatorComprehensiveTests).Assembly, + typeof(ArchitectureContext).Assembly); - // ✅ Freeze 容器 _container.Freeze(); _context = new ArchitectureContext(_container); @@ -194,19 +189,19 @@ public class MediatorComprehensiveTests /// - /// 测试未注册的Mediator抛出InvalidOperationException + /// 测试未注册的 CQRS handler 时抛出 InvalidOperationException /// [Test] - public void Unregistered_Mediator_Should_Throw_InvalidOperationException() + public void Unregistered_Cqrs_Handler_Should_Throw_InvalidOperationException() { - var containerWithoutMediator = new MicrosoftDiContainer(); - containerWithoutMediator.Freeze(); + var containerWithoutHandlers = new MicrosoftDiContainer(); + containerWithoutHandlers.Freeze(); - var contextWithoutMediator = new ArchitectureContext(containerWithoutMediator); + var contextWithoutHandlers = new ArchitectureContext(containerWithoutHandlers); var testRequest = new TestRequest { Value = 42 }; Assert.ThrowsAsync(async () => - await contextWithoutMediator.SendRequestAsync(testRequest)); + await contextWithoutHandlers.SendRequestAsync(testRequest)); } /// @@ -270,10 +265,10 @@ public class MediatorComprehensiveTests } /// - /// 测试并发Mediator请求不会相互干扰 + /// 测试并发 CQRS 请求不会相互干扰 /// [Test] - public async Task Concurrent_Mediator_Requests_Should_Not_Interfere() + public async Task Concurrent_Cqrs_Requests_Should_Not_Interfere() { const int requestCount = 10; var tasks = new List>(); @@ -389,10 +384,10 @@ public class MediatorComprehensiveTests } /// - /// 测试Mediator性能基准 + /// 测试 CQRS 性能基准 /// [Test] - public async Task Performance_Benchmark_For_Mediator() + public async Task Performance_Benchmark_For_Cqrs() { const int iterations = 1000; var stopwatch = Stopwatch.StartNew(); @@ -413,10 +408,10 @@ public class MediatorComprehensiveTests } /// - /// 测试Mediator和传统CQRS可以共存 + /// 测试自有 CQRS 和传统 CQRS 可以共存 /// [Test] - public async Task Mediator_And_Legacy_CQRS_Can_Coexist() + public async Task Cqrs_And_Legacy_CQRS_Can_Coexist() { // 使用传统方式 var legacyCommand = new TestLegacyCommand(); @@ -726,4 +721,4 @@ public sealed class TestStreamRequestHandler : IStreamRequestHandler /// 为服务容器设置上下文并执行扩展配置钩子。 - /// 这一步统一承接 Mediator 等容器扩展的接入点,避免 直接操作容器细节。 + /// 这一步统一承接 CQRS 运行时与容器扩展的接入点,避免 直接操作容器细节。 /// /// 当前架构上下文。 /// 可选的服务集合配置委托。 private void ConfigureServices(IArchitectureContext context, Action? configurator) { services.SetContext(context); + CqrsHandlerRegistrar.RegisterHandlers( + services.Container, + [architectureType.Assembly, typeof(ArchitectureContext).Assembly], + logger); if (configurator is null) - logger.Debug("Mediator-based cqrs will not take effect without the service setter configured!"); + logger.Debug("No external service configurator provided. Using built-in CQRS runtime registration only."); services.Container.ExecuteServicesHook(configurator); } @@ -115,4 +120,4 @@ internal sealed class ArchitectureBootstrapper( { await services.ModuleManager.InitializeAllAsync(asyncMode); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 1e3d72a7..77c04fcf 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -1,14 +1,19 @@ using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using Mediator; +using GFramework.Core.Cqrs.Internal; +using GFramework.Core.Logging; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Architectures; @@ -20,23 +25,15 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext { private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); private readonly ConcurrentDictionary _serviceCache = new(); + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContext)); + private CqrsDispatcher? _cqrsDispatcher; - #region Mediator Integration + #region CQRS Integration /// - /// 获取 Mediator 实例(延迟加载) + /// 获取 CQRS 运行时分发器(延迟初始化)。 /// - private IMediator Mediator => GetOrCache(); - - /// - /// 获取 ISender 实例(更轻量的发送器) - /// - private ISender Sender => GetOrCache(); - - /// - /// 获取 IPublisher 实例(用于发布通知) - /// - private IPublisher Publisher => GetOrCache(); + private CqrsDispatcher CqrsDispatcher => _cqrsDispatcher ??= new CqrsDispatcher(_container, this, _logger); /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 @@ -64,30 +61,23 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发送请求(Command/Query) - /// 这是推荐的新方式,统一处理命令和查询 + /// 发送请求(Command/Query) + /// 使用 GFramework 自有 CQRS runtime 统一处理命令和查询。 /// /// 响应类型 /// 请求对象(Command 或 Query) /// 取消令牌 /// 响应结果 - /// 当 Mediator 未注册时抛出 public async ValueTask SendRequestAsync( IRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - - var mediator = Mediator; - if (mediator == null) - throw new InvalidOperationException( - "Mediator not registered. Call EnableMediator() in your Architecture.OnInitialize() method."); - - return await mediator.Send(request, cancellationToken); + return await CqrsDispatcher.SendAsync(request, cancellationToken); } /// - /// [Mediator] 发送请求的同步版本(不推荐,仅用于兼容性) + /// 发送请求的同步版本(不推荐,仅用于兼容性) /// /// 响应类型 /// 请求对象 @@ -98,8 +88,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发布通知(一对多) - /// 用于事件驱动场景,多个处理器可以同时处理同一个通知 + /// 发布通知(一对多) + /// 使用 GFramework 自有 CQRS runtime 分发到所有已注册通知处理器。 /// /// 通知类型 /// 通知对象 @@ -110,16 +100,11 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext where TNotification : INotification { ArgumentNullException.ThrowIfNull(notification); - - var publisher = Publisher; - if (publisher == null) - throw new InvalidOperationException("Publisher not registered."); - - await publisher.Publish(notification, cancellationToken); + await CqrsDispatcher.PublishAsync(notification, cancellationToken); } /// - /// [Mediator] 发送请求并返回流(用于大数据集) + /// 发送请求并返回流(用于大数据集) /// /// 响应项类型 /// 流式请求 @@ -130,12 +115,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - - var mediator = Mediator; - if (mediator == null) - throw new InvalidOperationException("Mediator not registered."); - - return mediator.CreateStream(request, cancellationToken); + return CqrsDispatcher.CreateStream(request, cancellationToken); } /// @@ -180,12 +160,12 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// 发送 CQRS 查询的同步版本(不推荐,仅用于兼容性) /// /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(Mediator.IQuery query) + public TResponse SendQuery(GFramework.Core.Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -205,23 +185,17 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 异步发送查询并返回结果 - /// 通过Mediator模式发送查询请求,支持取消操作 + /// 异步发送 CQRS 查询并返回结果。 /// /// 查询响应类型 /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(Mediator.IQuery query, + public async ValueTask SendQueryAsync(GFramework.Core.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); - - var sender = Sender; - if (sender == null) - throw new InvalidOperationException("Sender not registered."); - - return await sender.Send(query, cancellationToken); + return await SendRequestAsync(query, cancellationToken); } #endregion @@ -347,23 +321,17 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext #region Command Execution /// - /// [Mediator] 异步发送命令并返回结果 - /// 通过Mediator模式发送命令请求,支持取消操作 + /// 异步发送 CQRS 命令并返回结果。 /// /// 命令响应类型 /// 要发送的命令对象 /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask - public async ValueTask SendCommandAsync(Mediator.ICommand command, + public async ValueTask SendCommandAsync(GFramework.Core.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); - - var sender = Sender; - if (sender == null) - throw new InvalidOperationException("Sender not registered."); - - return await sender.Send(command, cancellationToken); + return await SendRequestAsync(command, cancellationToken); } /// @@ -393,12 +361,12 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// 发送 CQRS 命令的同步版本(不推荐,仅用于兼容性) /// /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(Mediator.ICommand command) + public TResponse SendCommand(GFramework.Core.Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } @@ -491,4 +459,4 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractCommandWithInput.cs b/GFramework.Core/Command/AbstractCommandWithInput.cs index 5326fe6e..7512c760 100644 --- a/GFramework.Core/Command/AbstractCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractCommandWithInput.cs @@ -9,13 +9,13 @@ namespace GFramework.Core.Command; /// /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, ICommand +public abstract class AbstractCommand(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法 /// - void ICommand.Execute() + void GFramework.Core.Abstractions.Command.ICommand.Execute() { OnExecute(input); } @@ -25,4 +25,4 @@ public abstract class AbstractCommand(TInput input) : ContextAwareBase, /// /// 命令执行所需的输入参数 protected abstract void OnExecute(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractCommandWithResult.cs b/GFramework.Core/Command/AbstractCommandWithResult.cs index 67901821..7ecc9522 100644 --- a/GFramework.Core/Command/AbstractCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractCommandWithResult.cs @@ -10,14 +10,14 @@ namespace GFramework.Core.Command; /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行后返回的结果类型 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, ICommand +public abstract class AbstractCommand(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法 /// /// 命令执行后的结果 - TResult ICommand.Execute() + TResult GFramework.Core.Abstractions.Command.ICommand.Execute() { return OnExecute(input); } @@ -28,4 +28,4 @@ public abstract class AbstractCommand(TInput input) : ContextAw /// 命令执行所需的输入参数 /// 命令执行后的结果 protected abstract TResult OnExecute(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs index 4a56c775..4aaf797a 100644 --- a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs @@ -12,9 +12,9 @@ // limitations under the License. using System.Diagnostics; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; -using Mediator; namespace GFramework.Core.Cqrs.Behaviors; @@ -69,4 +69,4 @@ public sealed class LoggingBehavior : IPipelineBehavior : IPipelineBehavior } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs index 8f7b6112..0ffc7424 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs @@ -12,7 +12,8 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; @@ -21,7 +22,7 @@ namespace GFramework.Core.Cqrs.Command; /// 继承自ContextAwareBase并实现ICommandHandler接口,为具体的命令处理器提供基础功能 /// /// 命令类型 -public abstract class AbstractCommandHandler : ContextAwareBase, ICommandHandler +public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler where TCommand : ICommand { /// @@ -41,7 +42,7 @@ public abstract class AbstractCommandHandler : ContextAwareBase, IComm /// /// 命令类型,必须实现ICommand接口 /// 命令执行结果类型 -public abstract class AbstractCommandHandler : ContextAwareBase, ICommandHandler +public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler where TCommand : ICommand { /// @@ -52,4 +53,4 @@ public abstract class AbstractCommandHandler : ContextAwareBa /// 取消令牌,用于取消操作 /// 表示异步操作完成的ValueTask,包含命令执行结果 public abstract ValueTask Handle(TCommand command, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs index 064cb279..0f86f36c 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -12,7 +12,8 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; @@ -24,7 +25,7 @@ namespace GFramework.Core.Cqrs.Command; /// 流式命令类型,必须实现IStreamCommand接口 /// 流式命令响应元素类型 public abstract class AbstractStreamCommandHandler : ContextAwareBase, - IStreamCommandHandler + IStreamRequestHandler where TCommand : IStreamCommand { /// @@ -35,4 +36,4 @@ public abstract class AbstractStreamCommandHandler : Contex /// 取消令牌,用于取消流式处理操作 /// 异步可枚举的响应序列,每个元素类型为TResponse public abstract IAsyncEnumerable Handle(TCommand command, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Command/CommandBase.cs b/GFramework.Core/Cqrs/Command/CommandBase.cs index ba0e56ad..78fa134e 100644 --- a/GFramework.Core/Cqrs/Command/CommandBase.cs +++ b/GFramework.Core/Cqrs/Command/CommandBase.cs @@ -12,7 +12,6 @@ // limitations under the License. using GFramework.Core.Abstractions.Cqrs.Command; -using Mediator; namespace GFramework.Core.Cqrs.Command; @@ -29,4 +28,4 @@ public abstract class CommandBase(TInput input) : ICommand public TInput Input => input; -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs new file mode 100644 index 00000000..69f6794d --- /dev/null +++ b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs @@ -0,0 +1,263 @@ +using System.Collections.Concurrent; +using System.Reflection; +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Core.Cqrs.Internal; + +/// +/// GFramework 自有 CQRS 运行时分发器。 +/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前架构上下文。 +/// +internal sealed class CqrsDispatcher( + IIocContainer container, + IArchitectureContext context, + ILogger logger) +{ + private delegate ValueTask RequestInvoker(object handler, object request, CancellationToken cancellationToken); + private delegate ValueTask RequestPipelineInvoker( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken); + private delegate ValueTask NotificationInvoker(object handler, object notification, CancellationToken cancellationToken); + private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> RequestInvokers = new(); + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> RequestPipelineInvokers = new(); + private static readonly ConcurrentDictionary NotificationInvokers = new(); + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new(); + + /// + /// 发送请求并返回结果。 + /// + /// 响应类型。 + /// 请求对象。 + /// 取消令牌。 + /// 请求响应。 + public async ValueTask SendAsync( + IRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + var requestType = request.GetType(); + var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)); + var handler = container.Get(handlerType) + ?? throw new InvalidOperationException( + $"No CQRS request handler registered for {requestType.FullName}."); + + PrepareHandler(handler); + var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)); + var behaviors = container.GetAll(behaviorType); + + foreach (var behavior in behaviors) + PrepareHandler(behavior); + + if (behaviors.Count == 0) + { + var invoker = RequestInvokers.GetOrAdd( + (requestType, typeof(TResponse)), + static key => CreateRequestInvoker(key.RequestType, key.ResponseType)); + + var result = await invoker(handler, request, cancellationToken); + return result is null ? default! : (TResponse)result; + } + + var pipelineInvoker = RequestPipelineInvokers.GetOrAdd( + (requestType, typeof(TResponse)), + static key => CreateRequestPipelineInvoker(key.RequestType, key.ResponseType)); + + var pipelineResult = await pipelineInvoker(handler, behaviors, request, cancellationToken); + return pipelineResult is null ? default! : (TResponse)pipelineResult; + } + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 通知对象。 + /// 取消令牌。 + public async ValueTask PublishAsync( + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(notification); + + var notificationType = notification.GetType(); + var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); + var handlers = container.GetAll(handlerType); + + if (handlers.Count == 0) + { + logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); + return; + } + + var invoker = NotificationInvokers.GetOrAdd( + notificationType, + CreateNotificationInvoker); + + foreach (var handler in handlers) + { + PrepareHandler(handler); + await invoker(handler, notification, cancellationToken); + } + } + + /// + /// 创建流式请求并返回异步响应序列。 + /// + /// 响应元素类型。 + /// 流式请求对象。 + /// 取消令牌。 + /// 异步响应序列。 + public IAsyncEnumerable CreateStream( + IStreamRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + var requestType = request.GetType(); + var handlerType = typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)); + var handler = container.Get(handlerType) + ?? throw new InvalidOperationException( + $"No CQRS stream handler registered for {requestType.FullName}."); + + PrepareHandler(handler); + + var invoker = StreamInvokers.GetOrAdd( + (requestType, typeof(TResponse)), + static key => CreateStreamInvoker(key.RequestType, key.ResponseType)); + + return (IAsyncEnumerable)invoker(handler, request, cancellationToken); + } + + /// + /// 为上下文感知处理器注入当前架构上下文。 + /// + /// 处理器实例。 + private void PrepareHandler(object handler) + { + if (handler is IContextAware contextAware) + contextAware.SetContext(context); + } + + /// + /// 生成请求处理器调用委托,避免每次发送都重复反射。 + /// + private static RequestInvoker CreateRequestInvoker(Type requestType, Type responseType) + { + var method = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(requestType, responseType); + return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); + } + + /// + /// 生成带管道行为的请求处理委托,避免每次发送都重复反射。 + /// + private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType, Type responseType) + { + var method = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(requestType, responseType); + return (RequestPipelineInvoker)Delegate.CreateDelegate(typeof(RequestPipelineInvoker), method); + } + + /// + /// 生成通知处理器调用委托,避免每次发布都重复反射。 + /// + private static NotificationInvoker CreateNotificationInvoker(Type notificationType) + { + var method = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(notificationType); + return (NotificationInvoker)Delegate.CreateDelegate(typeof(NotificationInvoker), method); + } + + /// + /// 生成流式处理器调用委托,避免每次创建流都重复反射。 + /// + private static StreamInvoker CreateStreamInvoker(Type requestType, Type responseType) + { + var method = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeStreamHandler), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(requestType, responseType); + return (StreamInvoker)Delegate.CreateDelegate(typeof(StreamInvoker), method); + } + + /// + /// 执行已强类型化的请求处理器调用。 + /// + private static async ValueTask InvokeRequestHandlerAsync( + object handler, + object request, + CancellationToken cancellationToken) + where TRequest : IRequest + { + var typedHandler = (IRequestHandler)handler; + var typedRequest = (TRequest)request; + var result = await typedHandler.Handle(typedRequest, cancellationToken); + return result; + } + + /// + /// 执行包含管道行为链的请求处理。 + /// + private static async ValueTask InvokeRequestPipelineAsync( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken) + where TRequest : IRequest + { + var typedHandler = (IRequestHandler)handler; + var typedRequest = (TRequest)request; + + MessageHandlerDelegate next = + (message, token) => typedHandler.Handle(message, token); + + for (var i = behaviors.Count - 1; i >= 0; i--) + { + var behavior = (IPipelineBehavior)behaviors[i]; + var currentNext = next; + next = (message, token) => behavior.Handle(message, currentNext, token); + } + + var result = await next(typedRequest, cancellationToken); + return result; + } + + /// + /// 执行已强类型化的通知处理器调用。 + /// + private static ValueTask InvokeNotificationHandlerAsync( + object handler, + object notification, + CancellationToken cancellationToken) + where TNotification : INotification + { + var typedHandler = (INotificationHandler)handler; + var typedNotification = (TNotification)notification; + return typedHandler.Handle(typedNotification, cancellationToken); + } + + /// + /// 执行已强类型化的流式处理器调用。 + /// + private static object InvokeStreamHandler( + object handler, + object request, + CancellationToken cancellationToken) + where TRequest : IStreamRequest + { + var typedHandler = (IStreamRequestHandler)handler; + var typedRequest = (TRequest)request; + return typedHandler.Handle(typedRequest, cancellationToken); + } +} diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs new file mode 100644 index 00000000..9f69bc33 --- /dev/null +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -0,0 +1,81 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace GFramework.Core.Cqrs.Internal; + +/// +/// 在架构初始化期间扫描并注册 CQRS 处理器。 +/// 首批实现采用运行时反射扫描,优先满足“无需 AddMediator 即可工作”的迁移目标。 +/// +internal static class CqrsHandlerRegistrar +{ + /// + /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 + /// + /// 目标容器。 + /// 要扫描的程序集集合。 + /// 日志记录器。 + public static void RegisterHandlers( + IIocContainer container, + IEnumerable assemblies, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(assemblies); + ArgumentNullException.ThrowIfNull(logger); + + foreach (var assembly in assemblies.Distinct()) + { + RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger); + } + } + + /// + /// 注册单个程序集里的所有 CQRS 处理器映射。 + /// + private static void RegisterAssemblyHandlers(IServiceCollection services, Assembly assembly, ILogger logger) + { + foreach (var implementationType in assembly.GetTypes().Where(IsConcreteHandlerType)) + { + var handlerInterfaces = implementationType + .GetInterfaces() + .Where(IsSupportedHandlerInterface) + .ToList(); + + if (handlerInterfaces.Count == 0) + continue; + + foreach (var handlerInterface in handlerInterfaces) + { + services.AddSingleton(handlerInterface, implementationType); + logger.Debug( + $"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); + } + } + } + + /// + /// 判断指定类型是否可作为可实例化处理器。 + /// + private static bool IsConcreteHandlerType(Type type) + { + return type is { IsAbstract: false, IsInterface: false } && !type.ContainsGenericParameters; + } + + /// + /// 判断接口是否为当前运行时支持的 CQRS 处理器接口。 + /// + private static bool IsSupportedHandlerInterface(Type type) + { + if (!type.IsGenericType) + return false; + + var definition = type.GetGenericTypeDefinition(); + return definition == typeof(IRequestHandler<,>) || + definition == typeof(INotificationHandler<>) || + definition == typeof(IStreamRequestHandler<,>); + } +} diff --git a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs b/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs index de4772fb..1b1157ab 100644 --- a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs +++ b/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Notification; @@ -33,4 +33,4 @@ public abstract class AbstractNotificationHandler : ContextAwareB /// 取消令牌,用于取消操作 /// 表示异步操作完成的ValueTask public abstract ValueTask Handle(TNotification notification, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Notification/NotificationBase.cs b/GFramework.Core/Cqrs/Notification/NotificationBase.cs index 96e26ea1..f04488b9 100644 --- a/GFramework.Core/Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Core/Cqrs/Notification/NotificationBase.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Abstractions.Cqrs.Notification; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Notification; @@ -28,4 +28,4 @@ public abstract class NotificationBase(TInput input) : INotification whe /// 获取通知的输入数据。 /// public TInput Input => input; -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs index 4ce887cf..f861312c 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs @@ -12,7 +12,8 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; @@ -23,7 +24,7 @@ namespace GFramework.Core.Cqrs.Query; /// /// 查询类型,必须实现IQuery接口 /// 查询结果类型 -public abstract class AbstractQueryHandler : ContextAwareBase, IQueryHandler +public abstract class AbstractQueryHandler : ContextAwareBase, IRequestHandler where TQuery : IQuery { /// @@ -34,4 +35,4 @@ public abstract class AbstractQueryHandler : ContextAwareBase, /// 取消令牌,用于取消操作 /// 表示异步操作完成的ValueTask,包含查询结果 public abstract ValueTask Handle(TQuery query, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs index 50cf1817..015da1da 100644 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -12,7 +12,8 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; @@ -24,7 +25,7 @@ namespace GFramework.Core.Cqrs.Query; /// 流式查询类型,必须实现IStreamQuery接口 /// 流式查询响应元素类型 public abstract class AbstractStreamQueryHandler : ContextAwareBase, - IStreamQueryHandler + IStreamRequestHandler where TQuery : IStreamQuery { /// @@ -35,4 +36,4 @@ public abstract class AbstractStreamQueryHandler : ContextAwa /// 取消令牌,用于取消流式查询操作 /// 异步可枚举的响应序列,每个元素类型为TResponse public abstract IAsyncEnumerable Handle(TQuery query, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Core/Cqrs/Query/QueryBase.cs index 2ef0f34b..cb0d4782 100644 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ b/GFramework.Core/Cqrs/Query/QueryBase.cs @@ -12,7 +12,6 @@ // limitations under the License. using GFramework.Core.Abstractions.Cqrs.Query; -using Mediator; namespace GFramework.Core.Cqrs.Query; @@ -29,4 +28,4 @@ public abstract class QueryBase(TInput input) : IQuery public TInput Input => input; -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs b/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs index 88e9efad..4ef6a270 100644 --- a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs +++ b/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; @@ -21,7 +21,7 @@ namespace GFramework.Core.Cqrs.Request; /// 继承自ContextAwareBase并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[Unit]接口 -public abstract class AbstractRequestHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractRequestHandler : ContextAwareBase, IRequestHandler where TRequest : IRequest { /// @@ -49,4 +49,4 @@ public abstract class AbstractRequestHandler : ContextAware /// 取消令牌,用于取消操作 /// 表示异步操作的ValueTask,完成时返回处理结果 public abstract ValueTask Handle(TRequest request, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs b/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs index a6151b49..a15ed5d7 100644 --- a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs +++ b/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; @@ -35,4 +35,4 @@ public abstract class AbstractStreamRequestHandler : Contex /// 取消令牌,用于取消流式请求操作 /// 异步可枚举的响应序列,每个元素类型为TResponse public abstract IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Request/RequestBase.cs b/GFramework.Core/Cqrs/Request/RequestBase.cs index 8b878750..ce85784f 100644 --- a/GFramework.Core/Cqrs/Request/RequestBase.cs +++ b/GFramework.Core/Cqrs/Request/RequestBase.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Abstractions.Cqrs.Request; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; @@ -29,4 +29,4 @@ public abstract class RequestBase(TInput input) : IRequest public TInput Input => input; -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 6daccd26..881741e7 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,16 +1,15 @@ using GFramework.Core.Abstractions.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs.Command; namespace GFramework.Core.Extensions; /// -/// 提供对 IContextAware 接口的 Mediator 命令扩展方法 -/// 使用 Mediator 库的命令模式 +/// 提供对 IContextAware 接口的 CQRS 命令扩展方法。 /// public static class ContextAwareMediatorCommandExtensions { /// - /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// 发送命令的同步版本(不推荐,仅用于兼容性) /// /// 命令响应类型 /// 实现 IContextAware 接口的对象 @@ -28,7 +27,7 @@ public static class ContextAwareMediatorCommandExtensions } /// - /// [Mediator] 异步发送命令并返回结果 + /// 异步发送命令并返回结果 /// /// 命令响应类型 /// 实现 IContextAware 接口的对象 @@ -45,4 +44,4 @@ public static class ContextAwareMediatorCommandExtensions var context = contextAware.GetContext(); return context.SendCommandAsync(command, cancellationToken); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index fa0d699d..1661d87b 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,10 +1,10 @@ using GFramework.Core.Abstractions.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Extensions; /// -/// 提供对 IContextAware 接口的 Mediator 统一接口扩展方法 +/// 提供对 IContextAware 接口的 CQRS 统一接口扩展方法。 /// public static class ContextAwareMediatorExtensions { @@ -122,4 +122,4 @@ public static class ContextAwareMediatorExtensions var context = contextAware.GetContext(); return context.SendAsync(command, cancellationToken); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index cbdb01b4..5a4bfeb6 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,16 +1,15 @@ using GFramework.Core.Abstractions.Rule; -using Mediator; +using GFramework.Core.Abstractions.Cqrs.Query; namespace GFramework.Core.Extensions; /// -/// 提供对 IContextAware 接口的 Mediator 查询扩展方法 -/// 使用 Mediator 库的查询模式 +/// 提供对 IContextAware 接口的 CQRS 查询扩展方法。 /// public static class ContextAwareMediatorQueryExtensions { /// - /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// 发送查询的同步版本(不推荐,仅用于兼容性) /// /// 查询响应类型 /// 实现 IContextAware 接口的对象 @@ -27,7 +26,7 @@ public static class ContextAwareMediatorQueryExtensions } /// - /// [Mediator] 异步发送查询并返回结果 + /// 异步发送查询并返回结果 /// /// 查询响应类型 /// 实现 IContextAware 接口的对象 @@ -44,4 +43,4 @@ public static class ContextAwareMediatorQueryExtensions var context = contextAware.GetContext(); return context.SendQueryAsync(query, cancellationToken); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 2615ca52..dec4f267 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,10 +1,10 @@ using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Logging; using GFramework.Core.Rule; -using Mediator; using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Ioc; @@ -804,4 +804,4 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Query/AbstractQueryWithResult.cs b/GFramework.Core/Query/AbstractQueryWithResult.cs index cb46071a..2c87622a 100644 --- a/GFramework.Core/Query/AbstractQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractQueryWithResult.cs @@ -9,7 +9,7 @@ namespace GFramework.Core.Query; /// /// 查询输入参数的类型,必须实现IQueryInput接口 /// 查询结果的类型 -public abstract class AbstractQuery(TInput input) : ContextAwareBase, IQuery +public abstract class AbstractQuery(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery where TInput : IQueryInput { /// @@ -27,4 +27,4 @@ public abstract class AbstractQuery(TInput input) : ContextAwar /// 查询输入参数 /// 查询结果,类型为TResult protected abstract TResult OnDo(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets b/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets index 5116e4da..fdcf958e 100644 --- a/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets +++ b/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets @@ -18,9 +18,17 @@ - - - + + + + diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index f0ccc21b..d97d08a7 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -1,8 +1,10 @@ -using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Extensions; using GFramework.Core.Extensions; -using Mediator; namespace GFramework.Godot.Coroutine; @@ -104,4 +106,4 @@ public static class ContextAwareCoroutineExtensions .ToCoroutineEnumerator() .RunCoroutine(segment, tag); } -} \ No newline at end of file +} diff --git a/local-plan/todos/cqrs-rewrite-migration-tracking.md b/local-plan/todos/cqrs-rewrite-migration-tracking.md new file mode 100644 index 00000000..55317f7c --- /dev/null +++ b/local-plan/todos/cqrs-rewrite-migration-tracking.md @@ -0,0 +1,109 @@ +# CQRS 重写迁移跟踪 + +## 目标 + +围绕 `GFramework` 当前的双轨 CQRS 现状,完成一轮以“去 Mediator 外部依赖”为目标的架构迁移: + +- 将 `Mediator` 从 GFramework 公共 API 和运行时主路径中移除 +- 基于 GFramework 自有抽象重建正式 CQRS runtime、行为管道和注册机制 +- 保留 `EventBus` 作为框架级事件系统,不与 CQRS notification 混同 +- 让 `CoreGrid-Migration` 直连本地 `GFramework`,作为真实迁移验证工程 +- 为复杂迁移建立明确恢复点与进度追踪,避免上下文过长或中断后失去状态 + +## 当前恢复点 + +- 恢复点编号:`CQRS-REWRITE-RP-002` +- 当前阶段:`Phase 4` +- 当前焦点: + - 清理剩余 `Mediator` 包依赖与文档残留 + - 评估是否继续把协程扩展和测试项目中的 `Mediator.Abstractions` 完全移除 + - 规划第二阶段优化:代码生成注册、性能收敛、行为 API 命名统一 + +## 本轮计划 + +### Phase 0:工作流基础 + +- [x] 在 `local-plan/todos/` 建立本任务跟踪文档 +- [x] 在 `local-plan/traces/` 建立本任务追踪文档 +- [x] 将恢复点 / trace / subagent 协作规范写入 `AGENTS.md` + +### Phase 1:本地验证链路 + +- [x] 确认 `CoreGrid-Migration` 当前引用形态 +- [x] 将 `CoreGrid-Migration` 从 NuGet 包切到本地 `GFramework` 工程引用 +- [x] 让 `CoreGrid-Migration` 使用本地 Source Generator 而不是外部已发布版本 +- [x] 验证本地引用链路至少能完成 restore / build + +### Phase 2:CQRS 基础重建 + +- [x] 在 `GFramework.Core.Abstractions` 定义自有 CQRS 契约 +- [x] 在 `GFramework.Core` 落地 dispatcher / handler registry / behavior pipeline +- [x] 清理 `IArchitectureContext` 中对 `Mediator.*` 的公共签名依赖 +- [x] 设计 CQRS 模块启用方式,替代 `Configurator => AddMediator(...)` + +### Phase 3:接入迁移 + +- [x] 迁移 `GFramework.Core.Cqrs.*` 基类到新契约 +- [x] 迁移 `ContextAwareMediator*Extensions` 与协程扩展 +- [x] 迁移 `CoreGrid-Migration/scripts/cqrs/**` 到新契约 +- [x] 删除 `GameArchitecture.Configurator` 中的 `AddMediator(...)` + +### Phase 4:收尾 + +- [ ] 移除 `Mediator` 包依赖与相关测试/文档残留 +- [x] 运行目标构建与测试 +- [x] 记录剩余风险与下一恢复点 + +## 当前完成结果 + +- `CoreGrid-Migration` 已直连本地 `GFramework` 源码与本地 source generators。 +- `GameArchitecture` 已不再依赖 `collection.AddMediator(...)` 即可使用 CQRS。 +- `GFramework.Core.Abstractions` 新增自有 CQRS 契约: + - `IRequest` / `INotification` / `IStreamRequest` + - `IRequestHandler<,>` / `INotificationHandler<>` / `IStreamRequestHandler<,>` + - `Unit` + - `IPipelineBehavior<,>` / `MessageHandlerDelegate<,>` +- `ArchitectureBootstrapper` 会在初始化阶段自动扫描并注册当前架构程序集与 `GFramework.Core` 程序集中的 CQRS handlers。 +- `CqrsDispatcher` 已支持: + - request dispatch + - notification publish + - stream dispatch + - context-aware handler 注入 + - request pipeline behavior 链式执行 +- `GFramework.Core.Tests` 中原依赖 `Mediator` 注册路径的测试已切换到框架内建 CQRS 注册路径。 +- 当前验证状态: + - `dotnet build GFramework/GFramework.sln` 通过 + - `dotnet test GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj --no-build` 通过,`1621` 个测试全部通过 + - `dotnet build CoreGrid-Migration/CoreGrid.sln` 通过 + +## 当前已知事实 + +- `GFramework` 当前仍同时维护: + - 基于 `CommandExecutor` / `QueryExecutor` / `EventBus` 的轻量旧 CQRS + - 基于 GFramework 自有抽象的新 CQRS runtime +- 仍存在 `Mediator` 残留的区域主要集中在: + - 文档中的历史说明 + - `MediatorCoroutineExtensions` 及对应测试 + - 测试项目对 `Mediator.Abstractions` 的少量残余依赖 +- `CoreGrid-Migration` 已切到本地源码引用,并在当前恢复点完成构建验证 + +## 当前风险 + +- `GFramework` 仓库存在与本任务无关的既有改动,提交时必须避免覆盖 +- `CoreGrid-Migration` 是 worktree,WSL 下原生 `git` 解析该 worktree 路径有兼容问题 +- 当前 `RegisterMediatorBehavior` 命名仍保留历史前缀,但底层已切换为框架自有 CQRS pipeline;若后续要彻底脱媒介命名,需要一次 API 命名迁移 +- 当前 handler 自动注册基于运行时反射扫描;若后续追求冷启动与 AOT 友好性,需要补 source-generator 注册路径 + +## 下次恢复建议 + +若本轮中断,优先从以下顺序恢复: + +1. 查看 `local-plan/traces/cqrs-rewrite-migration-trace.md` +2. 确认当前恢复点 `CQRS-REWRITE-RP-002` 已对应到最新提交 +3. 优先决定是否继续移除 `Mediator.Abstractions` 包与 `MediatorCoroutineExtensions` 历史兼容层 +4. 若继续演进,再处理 CQRS 注册的生成器化与 API 命名统一 + +## 备注 + +- 本文档是当前任务的主恢复点,后续每个关键阶段完成后都要更新 +- 发生方向调整时,不覆盖旧结论,直接追加阶段记录与新的恢复点编号 diff --git a/local-plan/traces/cqrs-rewrite-migration-trace.md b/local-plan/traces/cqrs-rewrite-migration-trace.md new file mode 100644 index 00000000..1df972fe --- /dev/null +++ b/local-plan/traces/cqrs-rewrite-migration-trace.md @@ -0,0 +1,68 @@ +# CQRS 重写迁移追踪 + +## 2026-04-14 + +### 阶段:初始化 + +- 建立 `CQRS-REWRITE-RP-001` 恢复点 +- 已确认本次迁移目标: + - 彻底参考 `Mediator` 思路重写 GFramework 正式 CQRS + - 不保留对 `Mediator` 的兼容层 + - 使用 `abstractions + runtime 可选模块` 边界 + - 保留 `EventBus`,不与 CQRS notification 合并 + +### 已确认的实现前提 + +- `CoreGrid-Migration` 当前仍依赖 NuGet 版 `GeWuYou.GFramework*` +- `CoreGrid/scripts/core/GameArchitecture.cs` 与 `CoreGrid-Migration/scripts/core/GameArchitecture.cs` 通过 `AddMediator(...)` 启用基于生成器的 runtime +- `GFramework` 当前 `IArchitectureContext` 与一批 CQRS 基类直接引用 `Mediator.*` +- `CoreGrid/scripts/cqrs/**` 的 handler 很薄,主要迁移成本在框架 runtime 和注册机制,不在业务逻辑本身 + +### 当前动作 + +- 准备更新 `AGENTS.md`,补充恢复点 / trace / subagent 协作规范 +- 准备将 `CoreGrid-Migration` 切换为本地项目引用,建立真实验证链路 + +### 下一步 + +1. 完成 `AGENTS.md` 规则补充 +2. 改造 `CoreGrid-Migration/CoreGrid.csproj` 为本地项目与本地生成器引用 +3. 进行第一次构建验证,确认本地链路可用 + +### 阶段:CQRS 主路径迁移完成 + +- `CoreGrid-Migration/CoreGrid.csproj` 已切到本地 `ProjectReference` + 本地 source generators +- `CoreGrid-Migration/scripts/core/GameArchitecture.cs` 已删除 `AddMediator(...)` 配置钩子 +- `GFramework.Core.Abstractions` 新增 GFramework 自有 CQRS 契约与 `Unit` +- `IArchitectureContext` / `ArchitectureContext` 已切到自有 CQRS 签名 +- `ArchitectureBootstrapper` 已内建 handler 扫描注册,使用方无需再显式调用 `AddMediator(...)` +- `CqrsDispatcher` 已补齐 request/notification/stream dispatch 与 pipeline behavior 执行 +- `GFramework.Core.Cqrs.*` 基类、`ContextAwareMediator*Extensions`、Godot 协程上下文扩展均已迁到新契约 +- `GFramework.Core.Tests` 中原依赖旧 `Mediator` 注册入口的测试已迁移到 `CqrsTestRuntime` 反射注册路径 + +### 阶段:验证 + +- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.Core/GFramework.Core.csproj` + - 结果:通过 +- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj` + - 结果:通过 +- `dotnet test /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj --no-build` + - 结果:通过 + - 明细:`1621` 个测试全部通过 +- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.sln` + - 结果:通过 +- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/CoreGrid-Migration/CoreGrid.sln` + - 结果:通过 + - 备注:仅存在既有 analyzer warnings,无新增构建错误 + +### 当前残留 + +- 文档与少量历史 API 命名仍保留 `Mediator` 前缀 +- `MediatorCoroutineExtensions` 与少量测试仍依赖 `Mediator.Abstractions` +- handler 自动注册当前使用运行时反射扫描,尚未切回生成器注册 + +### 下一步建议 + +1. 决定是否继续做“完全移除 `Mediator.Abstractions` 包”的第二阶段清理 +2. 若继续,优先迁移协程扩展与相关测试 +3. 评估是否将 `RegisterMediatorBehavior`、`ContextAwareMediator*Extensions` 等历史命名升级为 CQRS 中性命名 From 618f07369e14a8ea5afe604c7ffb915d38bac1aa Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:56:11 +0800 Subject: [PATCH 02/70] =?UTF-8?q?config(ci):=20=E9=85=8D=E7=BD=AECoderabbi?= =?UTF-8?q?t=E4=BB=A5=E6=94=AF=E6=8C=81=E9=87=8D=E6=9E=84=E5=88=86?= =?UTF-8?q?=E6=94=AF=E7=9A=84=E8=87=AA=E5=8A=A8=E5=AE=A1=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启用auto_review功能以进行代码审查 - 添加refactor/cqrs-architecture-decoupling作为基础分支 - 配置草稿PR时不进行审查的选项 - 设置聊天自动回复功能 --- .coderabbit.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 8cb624ec..5fc95d9d 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -16,6 +16,8 @@ reviews: auto_review: enabled: true drafts: false # draft PR 不 review + base_branches: + - refactor/cqrs-architecture-decoupling chat: auto_reply: true From 195c8321a1224760bb98564a0cbf1b60408aa65d Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:37:32 +0800 Subject: [PATCH 03/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=9F=A5=E8=AF=A2=E8=B4=A3=E4=BB=BB=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E6=9E=B6=E6=9E=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现抽象命令处理器基类支持命令处理 - 添加流式命令处理器基类支持异步流式响应 - 创建查询处理器基类提供统一查询处理接口 - 实现查询基类提供通用查询结构定义 - 扩展架构上下文接口集成CQRS运行时入口 - 定义消息处理器委托支持管道行为处理 - 实现CQRS处理器注册器扫描并注册处理器 - 添加架构模块行为测试验证模块安装功能 - 创建中介器高级特性测试覆盖边界场景 --- .../Architectures/IArchitectureContext.cs | 99 ++++++----- .../Cqrs/MessageHandlerDelegate.cs | 5 + .../ArchitectureModulesBehaviorTests.cs | 8 +- .../Cqrs/CqrsHandlerRegistrarTests.cs | 165 ++++++++++++++++++ GFramework.Core.Tests/CqrsTestRuntime.cs | 60 +------ .../Mediator/MediatorAdvancedFeaturesTests.cs | 29 +-- .../MediatorArchitectureIntegrationTests.cs | 67 ++++++- .../Mediator/MediatorComprehensiveTests.cs | 28 +-- .../Cqrs/Command/AbstractCommandHandler.cs | 9 +- .../Command/AbstractStreamCommandHandler.cs | 6 +- .../Cqrs/Internal/CqrsHandlerRegistrar.cs | 72 +++++++- .../Cqrs/Query/AbstractQueryHandler.cs | 6 +- GFramework.Core/Cqrs/Query/QueryBase.cs | 2 +- .../todos/cqrs-rewrite-migration-tracking.md | 109 ------------ .../traces/cqrs-rewrite-migration-trace.md | 68 -------- 15 files changed, 415 insertions(+), 318 deletions(-) create mode 100644 GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs delete mode 100644 local-plan/todos/cqrs-rewrite-migration-tracking.md delete mode 100644 local-plan/traces/cqrs-rewrite-migration-trace.md diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index 9a08dec6..b9b5dc9a 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -1,7 +1,5 @@ using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Model; @@ -13,8 +11,13 @@ using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Abstractions.Architectures; /// -/// 架构上下文接口,提供对系统、模型、工具类的访问以及命令、查询、事件的发送和注册功能 +/// 架构上下文接口,统一暴露框架组件访问、兼容旧命令/查询总线,以及当前推荐的 CQRS 运行时入口。 /// +/// +/// 旧的 GFramework.Core.Abstractions.CommandGFramework.Core.Abstractions.Query 契约会继续通过原有 Command/Query Executor 路径执行,以保证存量代码兼容。 +/// 新的 GFramework.Core.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 +/// 新功能优先使用 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。 +/// public interface IArchitectureContext { /// @@ -106,85 +109,91 @@ public interface IArchitectureContext IReadOnlyList GetUtilitiesByPriority() where TUtility : class, IUtility; /// - /// 发送一个命令 + /// 发送一个旧版命令。 /// - /// 要发送的命令 + /// 要发送的旧版命令。 void SendCommand(ICommand command); /// - /// 发送一个带返回值的命令 + /// 发送一个旧版带返回值命令。 /// - /// 命令执行结果类型 - /// 要发送的命令 - /// 命令执行结果 - TResult SendCommand(Command.ICommand command); + /// 命令执行结果类型。 + /// 要发送的旧版命令。 + /// 命令执行结果。 + TResult SendCommand(ICommand command); /// - /// 发送一个 CQRS 命令并返回结果。 + /// 发送一个新版 CQRS 命令并返回结果。 /// /// 命令响应类型。 /// 要发送的 CQRS 命令。 /// 命令执行结果。 - TResponse SendCommand(GFramework.Core.Abstractions.Cqrs.Command.ICommand command); + /// + /// 这是迁移后的推荐命令入口。无返回值命令应实现 IRequest<Unit>,并优先通过 调用。 + /// + TResponse SendCommand(Cqrs.Command.ICommand command); /// - /// 发送并异步执行一个命令 + /// 异步发送一个旧版命令。 /// - /// 要发送的命令 + /// 要发送的旧版命令。 Task SendCommandAsync(IAsyncCommand command); /// - /// 异步发送一个 CQRS 命令并返回结果。 + /// 异步发送一个新版 CQRS 命令并返回结果。 /// /// 命令响应类型。 /// 要发送的 CQRS 命令。 /// 取消令牌。 /// 包含命令执行结果的值任务。 - ValueTask SendCommandAsync(GFramework.Core.Abstractions.Cqrs.Command.ICommand command, + ValueTask SendCommandAsync(Cqrs.Command.ICommand command, CancellationToken cancellationToken = default); /// - /// 发送并异步执行一个带返回值的命令 + /// 异步发送一个旧版带返回值命令。 /// - /// 命令执行结果类型 - /// 要发送的命令 - /// 命令执行结果 + /// 命令执行结果类型。 + /// 要发送的旧版命令。 + /// 命令执行结果。 Task SendCommandAsync(IAsyncCommand command); /// - /// 发送一个查询请求 + /// 发送一个旧版查询请求。 /// - /// 查询结果类型 - /// 要发送的查询 - /// 查询结果 - TResult SendQuery(Query.IQuery query); + /// 查询结果类型。 + /// 要发送的旧版查询。 + /// 查询结果。 + TResult SendQuery(IQuery query); /// - /// 发送一个 CQRS 查询并返回结果。 + /// 发送一个新版 CQRS 查询并返回结果。 /// /// 查询响应类型。 /// 要发送的 CQRS 查询。 /// 查询结果。 - TResponse SendQuery(GFramework.Core.Abstractions.Cqrs.Query.IQuery query); + /// + /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse>。 + /// + TResponse SendQuery(Cqrs.Query.IQuery query); /// - /// 异步发送一个查询请求 + /// 异步发送一个旧版查询请求。 /// - /// 查询结果类型 - /// 要发送的异步查询 - /// 查询结果 + /// 查询结果类型。 + /// 要发送的旧版异步查询。 + /// 查询结果。 Task SendQueryAsync(IAsyncQuery query); /// - /// 异步发送一个 CQRS 查询并返回结果。 + /// 异步发送一个新版 CQRS 查询并返回结果。 /// /// 查询响应类型。 /// 要发送的 CQRS 查询。 /// 取消令牌。 /// 包含查询结果的值任务。 - ValueTask SendQueryAsync(GFramework.Core.Abstractions.Cqrs.Query.IQuery query, + ValueTask SendQueryAsync(Cqrs.Query.IQuery query, CancellationToken cancellationToken = default); /// @@ -216,28 +225,40 @@ public interface IArchitectureContext void UnRegisterEvent(Action onEvent); /// - /// 发送请求(统一处理 Command/Query) + /// 发送新版 CQRS 请求,并统一处理命令与查询。 /// + /// + /// 这是自有 CQRS 运行时的主入口。新代码应优先通过该方法或 进入 dispatcher。 + /// ValueTask SendRequestAsync( IRequest request, CancellationToken cancellationToken = default); /// - /// 发送请求(同步版本,不推荐) + /// 发送新版 CQRS 请求的同步包装版本。 /// + /// + /// 仅为兼容同步调用链保留;新代码应优先使用异步入口,避免阻塞当前线程。 + /// TResponse SendRequest(IRequest request); /// - /// 发布通知(一对多事件) + /// 发布新版 CQRS 通知。 /// + /// + /// 该入口用于一对多通知分发,与框架级 EventBus 事件系统并存,适合围绕请求处理过程传播领域通知。 + /// ValueTask PublishAsync( TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification; /// - /// 创建流式请求(用于大数据集) + /// 创建新版 CQRS 流式请求。 /// + /// + /// 适用于需要按序惰性产出大量结果的场景。调用方应消费返回的异步序列,而不是回退到旧版查询总线。 + /// IAsyncEnumerable CreateStream( IStreamRequest request, CancellationToken cancellationToken = default); @@ -245,7 +266,7 @@ public interface IArchitectureContext // === 便捷扩展方法 === /// - /// 发送命令(无返回值) + /// 发送一个无返回值的新版 CQRS 命令。 /// ValueTask SendAsync( TCommand command, @@ -253,7 +274,7 @@ public interface IArchitectureContext where TCommand : IRequest; /// - /// 发送命令(有返回值) + /// 发送一个有返回值的新版 CQRS 请求。 /// ValueTask SendAsync( IRequest command, diff --git a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs index 172f7f3b..520f9fee 100644 --- a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs +++ b/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs @@ -3,6 +3,11 @@ namespace GFramework.Core.Abstractions.Cqrs; /// /// 表示 CQRS 请求在管道中继续向下执行的处理委托。 /// +/// +/// 管道行为可以通过不调用该委托来短路请求处理。 +/// 除显式实现重试等高级语义外,行为通常应最多调用一次该委托,以维持单次请求分发的确定性。 +/// 调用方应传递当前收到的 ,确保取消信号沿整条管道一致传播。 +/// /// 请求类型。 /// 响应类型。 /// 当前请求消息。 diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index 7485d978..bf6566b4 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -2,7 +2,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using GFramework.Core.Tests; using GfCqrs = GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -67,9 +66,7 @@ public class ArchitectureModulesBehaviorTests await architecture.InitializeAsync(); - var response = await CqrsTestRuntime.ExecutePipelineAsync( - architecture.Context, - new ModuleBehaviorRequest()); + var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); Assert.Multiple(() => { @@ -174,8 +171,7 @@ public sealed class TrackingPipelineBehavior : GfCqrs.IPipe /// 取消令牌。 /// 下游处理器的响应结果。 public async ValueTask Handle( - TRequest message, - GfCqrs.MessageHandlerDelegate next, + TRequest message, GfCqrs.MessageHandlerDelegate next, CancellationToken cancellationToken) { InvocationCount++; diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs new file mode 100644 index 00000000..43a6272a --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -0,0 +1,165 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Architectures; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Core.Tests.Logging; + +namespace GFramework.Core.Tests.Cqrs; + +/// +/// 验证 CQRS 处理器自动注册在顺序与容错层面的可观察行为。 +/// +[TestFixture] +internal sealed class CqrsHandlerRegistrarTests +{ + private static readonly MethodInfo RecoverLoadableTypesMethod = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)! + .GetMethod("RecoverLoadableTypes", + BindingFlags.NonPublic | + BindingFlags.Static)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar.RecoverLoadableTypes."); + + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + + /// + /// 初始化测试容器并重置共享状态。 + /// + [SetUp] + public void SetUp() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + DeterministicNotificationHandlerState.Reset(); + + _container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers( + _container, + typeof(CqrsHandlerRegistrarTests).Assembly, + typeof(ArchitectureContext).Assembly); + + _container.Freeze(); + _context = new ArchitectureContext(_container); + } + + /// + /// 清理测试过程中创建的上下文与共享状态。 + /// + [TearDown] + public void TearDown() + { + _context = null; + _container = null; + DeterministicNotificationHandlerState.Reset(); + } + + /// + /// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。 + /// + [Test] + public async Task PublishAsync_Should_Run_Notification_Handlers_In_Deterministic_Name_Order() + { + await _context!.PublishAsync(new DeterministicOrderNotification()); + + Assert.That( + DeterministicNotificationHandlerState.InvocationOrder, + Is.EqualTo( + [ + nameof(AlphaDeterministicNotificationHandler), + nameof(ZetaDeterministicNotificationHandler) + ])); + } + + /// + /// 验证部分类型加载失败时仍能保留可加载类型,并记录诊断日志。 + /// + [Test] + public void RecoverLoadableTypes_Should_Return_Loadable_Types_And_Log_Warnings() + { + var logger = new TestLogger(nameof(CqrsHandlerRegistrarTests), LogLevel.Warning); + var reflectionTypeLoadException = new ReflectionTypeLoadException( + [typeof(AlphaDeterministicNotificationHandler), null], + [new TypeLoadException("Missing optional dependency for registrar test.")]); + + var recoveredTypes = (IReadOnlyList)RecoverLoadableTypesMethod.Invoke( + null, + [typeof(CqrsHandlerRegistrarTests).Assembly, reflectionTypeLoadException, logger])!; + + Assert.Multiple(() => + { + Assert.That(recoveredTypes, Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + Assert.That(logger.Logs.Count(log => log.Level == LogLevel.Warning), Is.GreaterThanOrEqualTo(2)); + Assert.That( + logger.Logs.Any(log => log.Message.Contains("partially failed", StringComparison.Ordinal)), + Is.True); + Assert.That( + logger.Logs.Any(log => log.Message.Contains("Missing optional dependency", StringComparison.Ordinal)), + Is.True); + }); + } +} + +/// +/// 记录确定性通知处理器的实际执行顺序。 +/// +internal static class DeterministicNotificationHandlerState +{ + /// + /// 获取当前测试中的通知处理器执行顺序。 + /// + public static List InvocationOrder { get; } = []; + + /// + /// 重置共享的执行顺序状态。 + /// + public static void Reset() + { + InvocationOrder.Clear(); + } +} + +/// +/// 用于验证同一通知的多个处理器是否按稳定顺序执行。 +/// +internal sealed record DeterministicOrderNotification : INotification; + +/// +/// 故意放在 Alpha 之前声明,用于验证注册器不会依赖源码声明顺序。 +/// +internal sealed class ZetaDeterministicNotificationHandler : INotificationHandler +{ + /// + /// 记录当前处理器已执行。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + DeterministicNotificationHandlerState.InvocationOrder.Add(nameof(ZetaDeterministicNotificationHandler)); + return ValueTask.CompletedTask; + } +} + +/// +/// 名称排序上应先于 Zeta 处理器执行的通知处理器。 +/// +internal sealed class AlphaDeterministicNotificationHandler : INotificationHandler +{ + /// + /// 记录当前处理器已执行。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + DeterministicNotificationHandlerState.InvocationOrder.Add(nameof(AlphaDeterministicNotificationHandler)); + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs index ae1d3361..e2801cc8 100644 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ b/GFramework.Core.Tests/CqrsTestRuntime.cs @@ -1,22 +1,22 @@ using System.Reflection; -using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Abstractions.Rule; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using GfCqrs = GFramework.Core.Abstractions.Cqrs; namespace GFramework.Core.Tests; internal static class CqrsTestRuntime { private static readonly MethodInfo RegisterHandlersMethod = typeof(ArchitectureContext).Assembly - .GetType("GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)! - .GetMethod( - "RegisterHandlers", - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)! - ?? throw new InvalidOperationException("Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)! + .GetMethod( + "RegisterHandlers", + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) { @@ -28,46 +28,4 @@ internal static class CqrsTestRuntime null, [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]); } - - public static ValueTask ExecutePipelineAsync( - IArchitectureContext context, - TRequest request, - CancellationToken cancellationToken = default) - where TRequest : class, GfCqrs.IRequest - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(request); - - var handlers = context.GetServices>(); - if (handlers.Count == 0) - throw new InvalidOperationException( - $"No CQRS request handler registered for {typeof(TRequest).FullName}."); - - if (handlers.Count > 1) - throw new InvalidOperationException( - $"Expected a single CQRS request handler for {typeof(TRequest).FullName}, but found {handlers.Count}."); - - var handler = handlers[0]; - PrepareContext(handler, context); - - GfCqrs.MessageHandlerDelegate pipeline = handler.Handle; - - var behaviors = context.GetServices>(); - for (var index = behaviors.Count - 1; index >= 0; index--) - { - var behavior = behaviors[index]; - PrepareContext(behavior, context); - - var next = pipeline; - pipeline = (message, token) => behavior.Handle(message, next, token); - } - - return pipeline(request, cancellationToken); - } - - private static void PrepareContext(object instance, IArchitectureContext context) - { - if (instance is IContextAware contextAware) - contextAware.SetContext(context); - } } diff --git a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index d3bf4041..2dc2503a 100644 --- a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -4,7 +4,6 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using GFramework.Core.Tests; namespace GFramework.Core.Tests.Mediator; @@ -15,11 +14,16 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorAdvancedFeaturesTests { + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + [SetUp] public void SetUp() { LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); + TestCircuitBreakerHandler.Reset(); var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance); @@ -42,9 +46,6 @@ public class MediatorAdvancedFeaturesTests _container = null; } - private ArchitectureContext? _context; - private MicrosoftDiContainer? _container; - [Test] public async Task Request_With_Validation_Behavior_Should_Validate_Input() @@ -135,9 +136,6 @@ public class MediatorAdvancedFeaturesTests [Test] public async Task Circuit_Breaker_Should_Prevent_Cascading_Failures() { - TestCircuitBreakerHandler.FailureCount = 0; - TestCircuitBreakerHandler.SuccessCount = 0; - // 先触发几次失败 for (int i = 0; i < 5; i++) { @@ -275,12 +273,10 @@ public sealed class TestTransientErrorRequestHandler : IRequestHandler { - private static bool _circuitOpen = false; - public ValueTask Handle(TestCircuitBreakerRequest request, CancellationToken cancellationToken) { // 检查断路器状态 - if (_circuitOpen) + if (TestCircuitBreakerHandler.CircuitOpen) { throw new InvalidOperationException("Circuit breaker is open"); } @@ -292,7 +288,7 @@ public sealed class TestCircuitBreakerRequestHandler : IRequestHandler= 5) { - _circuitOpen = true; + TestCircuitBreakerHandler.CircuitOpen = true; } throw new InvalidOperationException("Service unavailable"); @@ -451,6 +447,17 @@ 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() + { + FailureCount = 0; + SuccessCount = 0; + CircuitOpen = false; + } } public sealed record TestCircuitBreakerRequest : IRequest diff --git a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index 056517f7..e176cce5 100644 --- a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -6,7 +6,7 @@ using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using GFramework.Core.Tests; +using GFramework.Core.Rule; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Mediator; @@ -18,11 +18,17 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorArchitectureIntegrationTests { + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + [SetUp] public void SetUp() { LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); + TestPerDispatchContextAwareHandler.Reset(); var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Instance); @@ -50,10 +56,6 @@ public class MediatorArchitectureIntegrationTests _commandBus = null; } - private ArchitectureContext? _context; - private MicrosoftDiContainer? _container; - private CommandExecutor? _commandBus; - [Test] public async Task Handler_Can_Access_Architecture_Context() { @@ -291,6 +293,20 @@ public class MediatorArchitectureIntegrationTests 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() + { + var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest()); + var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest()); + + Assert.Multiple(() => + { + Assert.That(firstResult, Is.Not.EqualTo(secondResult)); + Assert.That(TestPerDispatchContextAwareHandler.SeenInstanceIds, Is.EqualTo([firstResult, secondResult])); + Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context)); + }); + } } #region Integration Test Classes @@ -444,6 +460,42 @@ public sealed class TestMediatorRequestHandler : IRequestHandler +/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。 +/// +public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase, + IRequestHandler +{ + private static int _nextInstanceId; + private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId); + + public static List Contexts { get; } = []; + public static List SeenInstanceIds { get; } = []; + + /// + /// 记录当前实例编号与收到的架构上下文。 + /// + /// 请求实例。 + /// 取消令牌。 + /// 当前处理器实例编号。 + public ValueTask Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken) + { + Contexts.Add(Context); + SeenInstanceIds.Add(_instanceId); + return ValueTask.FromResult(_instanceId); + } + + /// + /// 重置跨测试共享的实例跟踪状态。 + /// + public static void Reset() + { + Contexts.Clear(); + SeenInstanceIds.Clear(); + _nextInstanceId = 0; + } +} + public sealed record TestContextAwareRequest : IRequest; public static class TestContextAwareHandler @@ -544,6 +596,11 @@ public sealed record TestMediatorRequest : IRequest public int Value { get; init; } } +/// +/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。 +/// +public sealed record TestPerDispatchContextAwareRequest : IRequest; + // 传统命令用于混合测试 public class TestTraditionalCommand : ICommand { diff --git a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs index 27522c16..27dfed5c 100644 --- a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs @@ -11,7 +11,6 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; -using GFramework.Core.Tests; using ICommand = GFramework.Core.Abstractions.Command.ICommand; using Unit = GFramework.Core.Abstractions.Cqrs.Unit; @@ -20,6 +19,15 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorComprehensiveTests { + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + /// /// 测试初始化方法,在每个测试方法执行前运行。 /// 负责初始化日志工厂、依赖注入容器、自有 CQRS 处理器以及各种总线服务。 @@ -74,14 +82,6 @@ public class MediatorComprehensiveTests _environment = null; } - private ArchitectureContext? _context; - private MicrosoftDiContainer? _container; - private EventBus? _eventBus; - private CommandExecutor? _commandBus; - private QueryExecutor? _queryBus; - private AsyncQueryExecutor? _asyncQueryBus; - private DefaultEnvironment? _environment; - /// /// 测试SendRequestAsync方法在请求有效时返回结果 /// @@ -418,7 +418,7 @@ public class MediatorComprehensiveTests _context!.SendCommand(legacyCommand); Assert.That(legacyCommand.Executed, Is.True); - // 使用Mediator方式 + // 使用自有 CQRS 方式 var mediatorCommand = new TestCommandWithResult { ResultValue = 999 }; var result = await _context.SendAsync(mediatorCommand); Assert.That(result, Is.EqualTo(999)); @@ -429,7 +429,7 @@ public class MediatorComprehensiveTests } } -#region Advanced Test Classes for Mediator Features +#region Advanced Test Classes for CQRS Features public sealed record TestLongRunningRequest : IRequest { @@ -623,9 +623,9 @@ public class TestLegacyCommand : ICommand #endregion -#region Test Classes - Mediator (新实现) +#region Test Classes - CQRS Runtime -// ✅ 这些类使用 Mediator.IRequest +// ✅ 这些类使用自有 CQRS IRequest public sealed record TestRequest : IRequest { public int Value { get; init; } @@ -657,7 +657,7 @@ public sealed record TestStreamRequest : IStreamRequest public int[] Values { get; init; } = []; } -// ✅ 这些 Handler 使用 Mediator.IRequestHandler +// ✅ 这些 Handler 使用自有 CQRS IRequestHandler public sealed class TestRequestHandler : IRequestHandler { public ValueTask Handle(TestRequest request, CancellationToken cancellationToken) diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs index 0ffc7424..528de106 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs @@ -11,15 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Rule; namespace GFramework.Core.Cqrs.Command; /// /// 抽象命令处理器基类 -/// 继承自ContextAwareBase并实现ICommandHandler接口,为具体的命令处理器提供基础功能 +/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 命令类型 public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler @@ -37,8 +38,8 @@ public abstract class AbstractCommandHandler : ContextAwareBase, IRequ /// /// 抽象命令处理器基类(带返回值版本) -/// 继承自ContextAwareBase并实现ICommandHandler接口,为具体的命令处理器提供基础功能 -/// 支持泛型命令和结果类型,实现CQRS模式中的命令处理 +/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 支持泛型命令和结果类型,框架会在每次分发前注入当前架构上下文。 /// /// 命令类型,必须实现ICommand接口 /// 命令执行结果类型 diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs index 0f86f36c..84e8e029 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -11,16 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Rule; namespace GFramework.Core.Cqrs.Command; /// /// 抽象流式命令处理器基类 -/// 继承自ContextAwareBase并实现IStreamCommandHandler接口,为具体的流式命令处理器提供基础功能 -/// 支持流式处理命令并产生异步可枚举的响应序列 +/// 继承自 ContextAwareBase 并实现 IStreamRequestHandler 接口,为具体的流式命令处理器提供基础功能。 +/// 支持流式处理命令并产生异步可枚举的响应序列,框架会在每次创建流前注入当前架构上下文。 /// /// 流式命令类型,必须实现IStreamCommand接口 /// 流式命令响应元素类型 diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs index 9f69bc33..136b76a7 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -2,7 +2,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Cqrs.Internal; @@ -27,7 +26,10 @@ internal static class CqrsHandlerRegistrar ArgumentNullException.ThrowIfNull(assemblies); ArgumentNullException.ThrowIfNull(logger); - foreach (var assembly in assemblies.Distinct()) + foreach (var assembly in assemblies + .Where(static assembly => assembly is not null) + .Distinct() + .OrderBy(GetAssemblySortKey, StringComparer.Ordinal)) { RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger); } @@ -38,11 +40,12 @@ internal static class CqrsHandlerRegistrar /// private static void RegisterAssemblyHandlers(IServiceCollection services, Assembly assembly, ILogger logger) { - foreach (var implementationType in assembly.GetTypes().Where(IsConcreteHandlerType)) + foreach (var implementationType in GetLoadableTypes(assembly, logger).Where(IsConcreteHandlerType)) { var handlerInterfaces = implementationType .GetInterfaces() .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) .ToList(); if (handlerInterfaces.Count == 0) @@ -50,13 +53,58 @@ internal static class CqrsHandlerRegistrar foreach (var handlerInterface in handlerInterfaces) { - services.AddSingleton(handlerInterface, implementationType); + // Request/notification handlers receive context injection before every dispatch. + // Transient registration avoids sharing mutable Context across concurrent requests. + services.AddTransient(handlerInterface, implementationType); logger.Debug( $"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); } } } + /// + /// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。 + /// + private static IReadOnlyList GetLoadableTypes(Assembly assembly, ILogger logger) + { + try + { + return assembly.GetTypes() + .Where(static type => type is not null) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + } + catch (ReflectionTypeLoadException exception) + { + return RecoverLoadableTypes(assembly, exception, logger); + } + } + + /// + /// 记录部分类型加载失败,并返回仍然可用的类型集合。 + /// + private static IReadOnlyList RecoverLoadableTypes( + Assembly assembly, + ReflectionTypeLoadException exception, + ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + logger.Warn( + $"CQRS handler scan partially failed for assembly {assemblyName}. Continuing with loadable types."); + + foreach (var loaderException in exception.LoaderExceptions.Where(static ex => ex is not null)) + { + logger.Warn( + $"Failed to load one or more types while scanning {assemblyName}: {loaderException!.Message}"); + } + + return exception.Types + .Where(static type => type is not null) + .Cast() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + } + /// /// 判断指定类型是否可作为可实例化处理器。 /// @@ -78,4 +126,20 @@ internal static class CqrsHandlerRegistrar definition == typeof(INotificationHandler<>) || definition == typeof(IStreamRequestHandler<,>); } + + /// + /// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。 + /// + private static string GetAssemblySortKey(Assembly assembly) + { + return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString(); + } + + /// + /// 生成类型排序键,保证同一程序集内的处理器与接口映射顺序稳定。 + /// + private static string GetTypeSortKey(Type type) + { + return type.FullName ?? type.Name; + } } diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs index f861312c..e9a3795f 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs @@ -11,16 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Core.Rule; namespace GFramework.Core.Cqrs.Query; /// /// 抽象查询处理器基类 -/// 继承自ContextAwareBase并实现IQueryHandler接口,为具体的查询处理器提供基础功能 -/// 支持泛型查询和结果类型,实现CQRS模式中的查询处理 +/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。 +/// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 查询类型,必须实现IQuery接口 /// 查询结果类型 diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Core/Cqrs/Query/QueryBase.cs index cb0d4782..6bccf549 100644 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ b/GFramework.Core/Cqrs/Query/QueryBase.cs @@ -17,7 +17,7 @@ namespace GFramework.Core.Cqrs.Query; /// /// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。 -/// 该类继承自 Mediator.IQuery<TResponse> 接口,提供了通用的查询结构。 +/// 该类实现 IQuery<TResponse> 接口,提供了通用的查询结构。 /// /// 查询输入数据的类型,必须实现 IQueryInput 接口 /// 查询执行后返回结果的类型 diff --git a/local-plan/todos/cqrs-rewrite-migration-tracking.md b/local-plan/todos/cqrs-rewrite-migration-tracking.md deleted file mode 100644 index 55317f7c..00000000 --- a/local-plan/todos/cqrs-rewrite-migration-tracking.md +++ /dev/null @@ -1,109 +0,0 @@ -# CQRS 重写迁移跟踪 - -## 目标 - -围绕 `GFramework` 当前的双轨 CQRS 现状,完成一轮以“去 Mediator 外部依赖”为目标的架构迁移: - -- 将 `Mediator` 从 GFramework 公共 API 和运行时主路径中移除 -- 基于 GFramework 自有抽象重建正式 CQRS runtime、行为管道和注册机制 -- 保留 `EventBus` 作为框架级事件系统,不与 CQRS notification 混同 -- 让 `CoreGrid-Migration` 直连本地 `GFramework`,作为真实迁移验证工程 -- 为复杂迁移建立明确恢复点与进度追踪,避免上下文过长或中断后失去状态 - -## 当前恢复点 - -- 恢复点编号:`CQRS-REWRITE-RP-002` -- 当前阶段:`Phase 4` -- 当前焦点: - - 清理剩余 `Mediator` 包依赖与文档残留 - - 评估是否继续把协程扩展和测试项目中的 `Mediator.Abstractions` 完全移除 - - 规划第二阶段优化:代码生成注册、性能收敛、行为 API 命名统一 - -## 本轮计划 - -### Phase 0:工作流基础 - -- [x] 在 `local-plan/todos/` 建立本任务跟踪文档 -- [x] 在 `local-plan/traces/` 建立本任务追踪文档 -- [x] 将恢复点 / trace / subagent 协作规范写入 `AGENTS.md` - -### Phase 1:本地验证链路 - -- [x] 确认 `CoreGrid-Migration` 当前引用形态 -- [x] 将 `CoreGrid-Migration` 从 NuGet 包切到本地 `GFramework` 工程引用 -- [x] 让 `CoreGrid-Migration` 使用本地 Source Generator 而不是外部已发布版本 -- [x] 验证本地引用链路至少能完成 restore / build - -### Phase 2:CQRS 基础重建 - -- [x] 在 `GFramework.Core.Abstractions` 定义自有 CQRS 契约 -- [x] 在 `GFramework.Core` 落地 dispatcher / handler registry / behavior pipeline -- [x] 清理 `IArchitectureContext` 中对 `Mediator.*` 的公共签名依赖 -- [x] 设计 CQRS 模块启用方式,替代 `Configurator => AddMediator(...)` - -### Phase 3:接入迁移 - -- [x] 迁移 `GFramework.Core.Cqrs.*` 基类到新契约 -- [x] 迁移 `ContextAwareMediator*Extensions` 与协程扩展 -- [x] 迁移 `CoreGrid-Migration/scripts/cqrs/**` 到新契约 -- [x] 删除 `GameArchitecture.Configurator` 中的 `AddMediator(...)` - -### Phase 4:收尾 - -- [ ] 移除 `Mediator` 包依赖与相关测试/文档残留 -- [x] 运行目标构建与测试 -- [x] 记录剩余风险与下一恢复点 - -## 当前完成结果 - -- `CoreGrid-Migration` 已直连本地 `GFramework` 源码与本地 source generators。 -- `GameArchitecture` 已不再依赖 `collection.AddMediator(...)` 即可使用 CQRS。 -- `GFramework.Core.Abstractions` 新增自有 CQRS 契约: - - `IRequest` / `INotification` / `IStreamRequest` - - `IRequestHandler<,>` / `INotificationHandler<>` / `IStreamRequestHandler<,>` - - `Unit` - - `IPipelineBehavior<,>` / `MessageHandlerDelegate<,>` -- `ArchitectureBootstrapper` 会在初始化阶段自动扫描并注册当前架构程序集与 `GFramework.Core` 程序集中的 CQRS handlers。 -- `CqrsDispatcher` 已支持: - - request dispatch - - notification publish - - stream dispatch - - context-aware handler 注入 - - request pipeline behavior 链式执行 -- `GFramework.Core.Tests` 中原依赖 `Mediator` 注册路径的测试已切换到框架内建 CQRS 注册路径。 -- 当前验证状态: - - `dotnet build GFramework/GFramework.sln` 通过 - - `dotnet test GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj --no-build` 通过,`1621` 个测试全部通过 - - `dotnet build CoreGrid-Migration/CoreGrid.sln` 通过 - -## 当前已知事实 - -- `GFramework` 当前仍同时维护: - - 基于 `CommandExecutor` / `QueryExecutor` / `EventBus` 的轻量旧 CQRS - - 基于 GFramework 自有抽象的新 CQRS runtime -- 仍存在 `Mediator` 残留的区域主要集中在: - - 文档中的历史说明 - - `MediatorCoroutineExtensions` 及对应测试 - - 测试项目对 `Mediator.Abstractions` 的少量残余依赖 -- `CoreGrid-Migration` 已切到本地源码引用,并在当前恢复点完成构建验证 - -## 当前风险 - -- `GFramework` 仓库存在与本任务无关的既有改动,提交时必须避免覆盖 -- `CoreGrid-Migration` 是 worktree,WSL 下原生 `git` 解析该 worktree 路径有兼容问题 -- 当前 `RegisterMediatorBehavior` 命名仍保留历史前缀,但底层已切换为框架自有 CQRS pipeline;若后续要彻底脱媒介命名,需要一次 API 命名迁移 -- 当前 handler 自动注册基于运行时反射扫描;若后续追求冷启动与 AOT 友好性,需要补 source-generator 注册路径 - -## 下次恢复建议 - -若本轮中断,优先从以下顺序恢复: - -1. 查看 `local-plan/traces/cqrs-rewrite-migration-trace.md` -2. 确认当前恢复点 `CQRS-REWRITE-RP-002` 已对应到最新提交 -3. 优先决定是否继续移除 `Mediator.Abstractions` 包与 `MediatorCoroutineExtensions` 历史兼容层 -4. 若继续演进,再处理 CQRS 注册的生成器化与 API 命名统一 - -## 备注 - -- 本文档是当前任务的主恢复点,后续每个关键阶段完成后都要更新 -- 发生方向调整时,不覆盖旧结论,直接追加阶段记录与新的恢复点编号 diff --git a/local-plan/traces/cqrs-rewrite-migration-trace.md b/local-plan/traces/cqrs-rewrite-migration-trace.md deleted file mode 100644 index 1df972fe..00000000 --- a/local-plan/traces/cqrs-rewrite-migration-trace.md +++ /dev/null @@ -1,68 +0,0 @@ -# CQRS 重写迁移追踪 - -## 2026-04-14 - -### 阶段:初始化 - -- 建立 `CQRS-REWRITE-RP-001` 恢复点 -- 已确认本次迁移目标: - - 彻底参考 `Mediator` 思路重写 GFramework 正式 CQRS - - 不保留对 `Mediator` 的兼容层 - - 使用 `abstractions + runtime 可选模块` 边界 - - 保留 `EventBus`,不与 CQRS notification 合并 - -### 已确认的实现前提 - -- `CoreGrid-Migration` 当前仍依赖 NuGet 版 `GeWuYou.GFramework*` -- `CoreGrid/scripts/core/GameArchitecture.cs` 与 `CoreGrid-Migration/scripts/core/GameArchitecture.cs` 通过 `AddMediator(...)` 启用基于生成器的 runtime -- `GFramework` 当前 `IArchitectureContext` 与一批 CQRS 基类直接引用 `Mediator.*` -- `CoreGrid/scripts/cqrs/**` 的 handler 很薄,主要迁移成本在框架 runtime 和注册机制,不在业务逻辑本身 - -### 当前动作 - -- 准备更新 `AGENTS.md`,补充恢复点 / trace / subagent 协作规范 -- 准备将 `CoreGrid-Migration` 切换为本地项目引用,建立真实验证链路 - -### 下一步 - -1. 完成 `AGENTS.md` 规则补充 -2. 改造 `CoreGrid-Migration/CoreGrid.csproj` 为本地项目与本地生成器引用 -3. 进行第一次构建验证,确认本地链路可用 - -### 阶段:CQRS 主路径迁移完成 - -- `CoreGrid-Migration/CoreGrid.csproj` 已切到本地 `ProjectReference` + 本地 source generators -- `CoreGrid-Migration/scripts/core/GameArchitecture.cs` 已删除 `AddMediator(...)` 配置钩子 -- `GFramework.Core.Abstractions` 新增 GFramework 自有 CQRS 契约与 `Unit` -- `IArchitectureContext` / `ArchitectureContext` 已切到自有 CQRS 签名 -- `ArchitectureBootstrapper` 已内建 handler 扫描注册,使用方无需再显式调用 `AddMediator(...)` -- `CqrsDispatcher` 已补齐 request/notification/stream dispatch 与 pipeline behavior 执行 -- `GFramework.Core.Cqrs.*` 基类、`ContextAwareMediator*Extensions`、Godot 协程上下文扩展均已迁到新契约 -- `GFramework.Core.Tests` 中原依赖旧 `Mediator` 注册入口的测试已迁移到 `CqrsTestRuntime` 反射注册路径 - -### 阶段:验证 - -- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.Core/GFramework.Core.csproj` - - 结果:通过 -- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj` - - 结果:通过 -- `dotnet test /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj --no-build` - - 结果:通过 - - 明细:`1621` 个测试全部通过 -- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/GFramework.sln` - - 结果:通过 -- `dotnet build /mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/CoreGrid-Migration/CoreGrid.sln` - - 结果:通过 - - 备注:仅存在既有 analyzer warnings,无新增构建错误 - -### 当前残留 - -- 文档与少量历史 API 命名仍保留 `Mediator` 前缀 -- `MediatorCoroutineExtensions` 与少量测试仍依赖 `Mediator.Abstractions` -- handler 自动注册当前使用运行时反射扫描,尚未切回生成器注册 - -### 下一步建议 - -1. 决定是否继续做“完全移除 `Mediator.Abstractions` 包”的第二阶段清理 -2. 若继续,优先迁移协程扩展与相关测试 -3. 评估是否将 `RegisterMediatorBehavior`、`ContextAwareMediator*Extensions` 等历史命名升级为 CQRS 中性命名 From f8fa2a848116d3ebad1686628ea64d992ab3d3ca Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:05:20 +0800 Subject: [PATCH 04/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=91=BD=E4=BB=A4=E5=A4=84=E7=90=86=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 AbstractStreamCommandHandler 基类支持流式命令处理 - 创建 CqrsHandlerRegistrar 自动扫描注册 CQRS 处理器 - 添加流式处理器接口 IStreamRequestHandler 支持 - 实现处理器注册的容错机制和类型加载恢复 - 添加确定性排序确保跨环境稳定的处理器注册顺序 - 提供完整的单元测试验证注册行为和异常处理 --- .../Cqrs/CqrsHandlerRegistrarTests.cs | 114 +++++++++++++----- GFramework.Core.Tests/CqrsTestRuntime.cs | 37 +++++- .../Command/AbstractStreamCommandHandler.cs | 28 +++-- 3 files changed, 134 insertions(+), 45 deletions(-) diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 43a6272a..28b1fb32 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -14,18 +14,7 @@ namespace GFramework.Core.Tests.Cqrs; [TestFixture] internal sealed class CqrsHandlerRegistrarTests { - private static readonly MethodInfo RecoverLoadableTypesMethod = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", - throwOnError: true)! - .GetMethod("RecoverLoadableTypes", - BindingFlags.NonPublic | - BindingFlags.Static)! - ?? throw new InvalidOperationException( - "Failed to locate CqrsHandlerRegistrar.RecoverLoadableTypes."); - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; /// @@ -40,8 +29,7 @@ internal sealed class CqrsHandlerRegistrarTests _container = new MicrosoftDiContainer(); CqrsTestRuntime.RegisterHandlers( _container, - typeof(CqrsHandlerRegistrarTests).Assembly, - typeof(ArchitectureContext).Assembly); + typeof(CqrsHandlerRegistrarTests).Assembly); _container.Freeze(); _context = new ArchitectureContext(_container); @@ -79,28 +67,53 @@ internal sealed class CqrsHandlerRegistrarTests /// 验证部分类型加载失败时仍能保留可加载类型,并记录诊断日志。 /// [Test] - public void RecoverLoadableTypes_Should_Return_Loadable_Types_And_Log_Warnings() + public void RegisterHandlers_Should_Register_Loadable_Types_And_Log_Warnings_When_Assembly_Load_Partially_Fails() { - var logger = new TestLogger(nameof(CqrsHandlerRegistrarTests), LogLevel.Warning); + var originalProvider = LoggerFactoryResolver.Provider; + var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning); var reflectionTypeLoadException = new ReflectionTypeLoadException( [typeof(AlphaDeterministicNotificationHandler), null], [new TypeLoadException("Missing optional dependency for registrar test.")]); + var partiallyLoadableAssembly = new Mock(); + partiallyLoadableAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.PartiallyLoadableAssembly, Version=1.0.0.0"); + partiallyLoadableAssembly + .Setup(static assembly => assembly.GetTypes()) + .Throws(reflectionTypeLoadException); - var recoveredTypes = (IReadOnlyList)RecoverLoadableTypesMethod.Invoke( - null, - [typeof(CqrsHandlerRegistrarTests).Assembly, reflectionTypeLoadException, logger])!; - - Assert.Multiple(() => + LoggerFactoryResolver.Provider = capturingProvider; + try { - Assert.That(recoveredTypes, Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); - Assert.That(logger.Logs.Count(log => log.Level == LogLevel.Warning), Is.GreaterThanOrEqualTo(2)); - Assert.That( - logger.Logs.Any(log => log.Message.Contains("partially failed", StringComparison.Ordinal)), - Is.True); - Assert.That( - logger.Logs.Any(log => log.Message.Contains("Missing optional dependency", StringComparison.Ordinal)), - Is.True); - }); + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, partiallyLoadableAssembly.Object); + container.Freeze(); + + var handlers = container.GetAll>(); + var warningLogs = capturingProvider.Loggers + .SelectMany(static logger => logger.Logs) + .Where(static log => log.Level == LogLevel.Warning) + .ToList(); + + Assert.Multiple(() => + { + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + Assert.That(warningLogs.Count, Is.GreaterThanOrEqualTo(2)); + Assert.That( + warningLogs.Any(log => log.Message.Contains("partially failed", StringComparison.Ordinal)), + Is.True); + Assert.That( + warningLogs.Any(log => + log.Message.Contains("Missing optional dependency", StringComparison.Ordinal)), + Is.True); + }); + } + finally + { + LoggerFactoryResolver.Provider = originalProvider; + } } } @@ -163,3 +176,46 @@ internal sealed class AlphaDeterministicNotificationHandler : INotificationHandl return ValueTask.CompletedTask; } } + +/// +/// 为 CQRS 注册测试捕获真实启动路径中创建的日志记录器。 +/// +/// +/// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。 +/// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。 +/// +internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider +{ + private readonly List _loggers = []; + + /// + /// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。 + /// + /// 要应用到新建测试日志器的最小日志级别。 + public CapturingLoggerFactoryProvider(LogLevel minLevel = LogLevel.Info) + { + MinLevel = minLevel; + } + + /// + /// 获取通过当前提供程序创建的全部测试日志器。 + /// + public IReadOnlyList Loggers => _loggers; + + /// + /// 获取或设置新建测试日志器的最小日志级别。 + /// + public LogLevel MinLevel { get; set; } + + /// + /// 创建一个测试日志器并将其纳入捕获集合。 + /// + /// 日志记录器名称。 + /// 用于后续断言的测试日志器。 + public ILogger CreateLogger(string name) + { + var logger = new TestLogger(name, MinLevel); + _loggers.Add(logger); + return logger; + } +} diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs index e2801cc8..e9664925 100644 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ b/GFramework.Core.Tests/CqrsTestRuntime.cs @@ -1,24 +1,49 @@ using System.Reflection; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; namespace GFramework.Core.Tests; +/// +/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 +/// +/// +/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, +/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 +/// internal static class CqrsTestRuntime { - private static readonly MethodInfo RegisterHandlersMethod = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", - throwOnError: true)! + private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar type."); + + private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType .GetMethod( "RegisterHandlers", BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static)! + BindingFlags.Static, + binder: null, + [ + typeof(IIocContainer), + typeof(IEnumerable), + typeof(ILogger) + ], + modifiers: null) ?? throw new InvalidOperationException( "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); - public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) + /// + /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 + /// + /// 承载处理器映射的测试容器。 + /// 要扫描的程序集集合。 + internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) { ArgumentNullException.ThrowIfNull(container); ArgumentNullException.ThrowIfNull(assemblies); diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs index 84e8e029..847563c4 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -18,22 +18,30 @@ using GFramework.Core.Rule; namespace GFramework.Core.Cqrs.Command; /// -/// 抽象流式命令处理器基类 -/// 继承自 ContextAwareBase 并实现 IStreamRequestHandler 接口,为具体的流式命令处理器提供基础功能。 -/// 支持流式处理命令并产生异步可枚举的响应序列,框架会在每次创建流前注入当前架构上下文。 +/// 抽象流式命令处理器基类。 +/// 继承自 并实现 , +/// 为具体的流式命令处理器提供基础功能。 /// -/// 流式命令类型,必须实现IStreamCommand接口 -/// 流式命令响应元素类型 +/// 流式命令类型,必须实现 +/// 流式命令响应元素类型。 +/// +/// 框架会在每次调用 CreateStream 进入实际处理逻辑前,为当前处理器实例注入架构上下文, +/// 因此派生类只能在 执行期间及其返回的异步枚举序列内假定 Context 可用。 +/// 默认注册器会将流式命令处理器注册为瞬态服务,以避免同一个上下文感知实例在多个流或并发请求之间复用。 +/// 派生类不应缓存处理器实例,也不应把依赖当前上下文的可变状态泄漏到流外部。 +/// 传入 的取消令牌同时约束流的创建与后续枚举, +/// 派生类应在启动阶段和每次生成响应前尊重取消请求,避免在调用方停止枚举后继续执行后台工作。 +/// public abstract class AbstractStreamCommandHandler : ContextAwareBase, IStreamRequestHandler where TCommand : IStreamCommand { /// - /// 处理流式命令并返回异步可枚举的响应序列 - /// 由具体的流式命令处理器子类实现流式处理逻辑 + /// 处理流式命令并返回异步可枚举的响应序列。 + /// 由具体的流式命令处理器子类实现流式处理逻辑。 /// - /// 要处理的流式命令对象 - /// 取消令牌,用于取消流式处理操作 - /// 异步可枚举的响应序列,每个元素类型为TResponse + /// 要处理的流式命令对象。 + /// 取消令牌,用于取消流式处理操作。 + /// 异步可枚举的响应序列,每个元素类型为 public abstract IAsyncEnumerable Handle(TCommand command, CancellationToken cancellationToken); } From 5c112f85456e5dfb99b49adba1b6f4a40f60bdd0 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:30:59 +0800 Subject: [PATCH 05/70] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=92=8C=E6=A0=B8=E5=BF=83=E6=A1=86=E6=9E=B6=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 详细文档,介绍命令查询职责分离模式 - 添加核心框架概述文档,包含架构图和快速开始指南 - 详细介绍五层架构设计和组件联动机制 - 提供完整的最佳实践和设计理念说明 - 添加架构生命周期管理和模块化设计说明 --- CLAUDE.md | 3 +- .../Architectures/IArchitecture.cs | 7 +- .../GFramework.Core.Abstractions.csproj | 2 +- .../Ioc/IIocContainer.cs | 7 +- .../ArchitectureServicesTests.cs | 31 +++--- .../Architectures/GameContextTests.cs | 28 +++--- .../MediatorCoroutineExtensionsTests.cs | 96 ++++++++----------- .../GFramework.Core.Tests.csproj | 5 - GFramework.Core/Architectures/Architecture.cs | 7 +- .../Architectures/ArchitectureBootstrapper.cs | 3 +- .../Architectures/ArchitectureModules.cs | 10 +- .../Extensions/MediatorCoroutineExtensions.cs | 22 ++--- .../Cqrs/Internal/CqrsHandlerRegistrar.cs | 2 +- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 7 +- docs/zh-CN/core/cqrs.md | 53 +++++----- docs/zh-CN/core/index.md | 7 +- 16 files changed, 128 insertions(+), 162 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 570606c7..1962098f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,8 @@ Architecture 负责统一生命周期编排,核心阶段包括: ### CQRS -命令与查询分离,支持同步与异步执行。Mediator 模式通过源码生成器集成,以减少模板代码并保持调用路径清晰。 +命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史 +`Mediator` 命名以兼容旧调用点。 ### EventBus diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index 0055d3fe..bb855189 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -2,7 +2,6 @@ using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Architectures; @@ -73,8 +72,8 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia void RegisterUtility(Action? onCreated = null) where T : class, IUtility; /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 + /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, /// 也支持绑定到单一请求/响应对的封闭行为类型。 /// @@ -101,4 +100,4 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// /// 表示异步等待操作的任务 Task WaitUntilReadyAsync(); -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index 8ec77b72..84d53f63 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -26,6 +26,6 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index b61d0f3a..d00211ed 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -1,6 +1,5 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Ioc; @@ -90,8 +89,8 @@ public interface IIocContainer : IContextAware void RegisterFactory(Func factory) where TService : class; /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑 + /// 注册 CQRS 请求管道行为。 + /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// /// 行为类型,必须是引用类型 void RegisterMediatorBehavior() @@ -227,4 +226,4 @@ public interface IIocContainer : IContextAware IServiceScope CreateScope(); #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs index 512e8a55..94b749b6 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs @@ -1,5 +1,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -13,7 +14,6 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; -using Mediator; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Architectures; @@ -34,6 +34,10 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureServicesTests { + private TestArchitectureContextV3? _context; + + private ArchitectureServices? _services; + [SetUp] public void SetUp() { @@ -41,9 +45,6 @@ public class ArchitectureServicesTests _context = new TestArchitectureContextV3(); } - private ArchitectureServices? _services; - private TestArchitectureContextV3? _context; - private void RegisterBuiltInServices() { _services!.ModuleManager.RegisterBuiltInModules(_services.Container); @@ -347,61 +348,59 @@ public class TestArchitectureContextV3 : IArchitectureContext { } - public ValueTask SendRequestAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest request, + public ValueTask SendRequestAsync(IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendRequest(global::GFramework.Core.Abstractions.Cqrs.IRequest request) + public TResponse SendRequest(IRequest request) { throw new NotImplementedException(); } - public ValueTask SendCommandAsync( - global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command, + public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync( - global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query, + public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } public ValueTask PublishAsync(TNotification notification, - CancellationToken cancellationToken = default) where TNotification : global::GFramework.Core.Abstractions.Cqrs.INotification + CancellationToken cancellationToken = default) where TNotification : INotification { throw new NotImplementedException(); } public IAsyncEnumerable CreateStream( - global::GFramework.Core.Abstractions.Cqrs.IStreamRequest request, + IStreamRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : global::GFramework.Core.Abstractions.Cqrs.IRequest + where TCommand : IRequest { throw new NotImplementedException(); } - public ValueTask SendAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest command, + public ValueTask SendAsync(IRequest command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index 6c2670bc..e11f0892 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -1,5 +1,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -13,7 +14,6 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; -using Mediator; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Architectures; @@ -394,61 +394,59 @@ public class TestArchitectureContext : IArchitectureContext { } - public ValueTask SendRequestAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest request, + public ValueTask SendRequestAsync(IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendRequest(global::GFramework.Core.Abstractions.Cqrs.IRequest request) + public TResponse SendRequest(IRequest request) { throw new NotImplementedException(); } - public ValueTask SendCommandAsync( - global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command, + public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(global::GFramework.Core.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync( - global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query, + public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(global::GFramework.Core.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } public ValueTask PublishAsync(TNotification notification, - CancellationToken cancellationToken = default) where TNotification : global::GFramework.Core.Abstractions.Cqrs.INotification + CancellationToken cancellationToken = default) where TNotification : INotification { throw new NotImplementedException(); } public IAsyncEnumerable CreateStream( - global::GFramework.Core.Abstractions.Cqrs.IStreamRequest request, + IStreamRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : global::GFramework.Core.Abstractions.Cqrs.IRequest + where TCommand : IRequest { throw new NotImplementedException(); } - public ValueTask SendAsync(global::GFramework.Core.Abstractions.Cqrs.IRequest command, + public ValueTask SendAsync(IRequest command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -468,7 +466,7 @@ public class TestArchitectureContext : IArchitectureContext /// 返回值类型 /// 命令对象 /// 命令执行结果 - public TResult SendCommand(Abstractions.Command.ICommand command) + public TResult SendCommand(ICommand command) { return default!; } @@ -489,7 +487,7 @@ public class TestArchitectureContext : IArchitectureContext /// 查询结果类型 /// 查询对象 /// 查询结果 - public TResult SendQuery(Abstractions.Query.IQuery query) + public TResult SendQuery(IQuery query) { return default!; } diff --git a/GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs b/GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs index eeb1493c..9dfa7b1e 100644 --- a/GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs +++ b/GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs @@ -13,68 +13,32 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Coroutine; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine.Extensions; -using Mediator; -using Moq; namespace GFramework.Core.Tests.Coroutine; /// -/// MediatorCoroutineExtensions的单元测试类 -/// 测试Mediator模式与协程集成的扩展方法 -/// 注意:由于 Mediator 使用源生成器,本测试类主要验证接口和参数验证 +/// 的单元测试类。 +/// 验证历史命名的协程扩展已切到框架内建 CQRS runtime, +/// 并确保协程对命令调度异常的传播行为保持稳定。 /// [TestFixture] public class MediatorCoroutineExtensionsTests { - /// - /// 测试用的简单命令类 - /// - private class TestCommand : IRequest - { - public string Data { get; set; } = string.Empty; - } - - /// - /// 测试用的简单事件类 - /// - private class TestEvent - { - public string Data { get; set; } = string.Empty; - } - - /// - /// 上下文感知基类的模拟实现 - /// - private class TestContextAware : IContextAware - { - public readonly Mock _mockContext = new(); - - public IArchitectureContext GetContext() - { - return _mockContext.Object; - } - - public void SetContext(IArchitectureContext context) - { - } - } - /// /// 验证SendCommandCoroutine应该返回IEnumerator /// [Test] public void SendCommandCoroutine_Should_Return_IEnumerator_Of_YieldInstruction() { - var command = new TestCommand { Data = "Test" }; + var command = new TestCommand("Test"); var contextAware = new TestContextAware(); - // 创建 mediator 模拟 - var mediatorMock = new Mock(); - contextAware._mockContext - .Setup(ctx => ctx.GetService()) - .Returns(mediatorMock.Object); + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(ValueTask.CompletedTask); var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command); @@ -82,23 +46,45 @@ public class MediatorCoroutineExtensionsTests } /// - /// 验证SendCommandCoroutine应该在mediator为null时抛出NullReferenceException + /// 验证 SendCommandCoroutine 在底层命令调度失败时会重新抛出原始异常。 /// [Test] - public void SendCommandCoroutine_Should_Throw_When_Mediator_Null() + public void SendCommandCoroutine_Should_Rethrow_Inner_Exception_When_Command_Fails() { - var command = new TestCommand { Data = "Test" }; + var command = new TestCommand("Test"); var contextAware = new TestContextAware(); + var expectedException = new InvalidOperationException("Command failed."); - // 设置上下文服务以返回null mediator - contextAware._mockContext - .Setup(ctx => ctx.GetService()) - .Returns((IMediator?)null); + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromException(expectedException))); - // 创建协程 var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command); - // 调用 MoveNext 时应该抛出 NullReferenceException - Assert.Throws(() => coroutine.MoveNext()); + Assert.That(coroutine.MoveNext(), Is.True); + var exception = Assert.Throws(() => coroutine.MoveNext()); + Assert.That(exception, Is.SameAs(expectedException)); } -} \ No newline at end of file + + /// + /// 测试用的简单命令类 + /// + private sealed record TestCommand(string Data) : IRequest; + + /// + /// 上下文感知基类的模拟实现 + /// + private sealed class TestContextAware : IContextAware + { + public Mock MockContext { get; } = new(); + + public IArchitectureContext GetContext() + { + return MockContext.Object; + } + + public void SetContext(IArchitectureContext context) + { + } + } +} diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index 5e59b144..97663fe0 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -10,11 +10,6 @@ 0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 39b4fd13..ac4fa2dc 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -7,7 +7,6 @@ using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Environment; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Architectures; @@ -146,8 +145,8 @@ public abstract class Architecture : IArchitecture #region Module Management /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 + /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 可以传入开放泛型行为类型,也可以传入绑定到特定请求的封闭行为类型。 /// /// 行为类型,必须是引用类型 @@ -328,4 +327,4 @@ public abstract class Architecture : IArchitecture } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs index f658329a..d022c806 100644 --- a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs +++ b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs @@ -2,7 +2,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Cqrs.Internal; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Architectures; @@ -23,7 +22,7 @@ internal sealed class ArchitectureBootstrapper( /// 因为用户初始化逻辑通常会立即访问事件总线、查询执行器或环境对象。 /// /// 调用方已经提供的上下文;如果为空则创建默认上下文。 - /// 可选的容器配置委托,用于接入 Mediator 等扩展服务。 + /// 可选的容器配置委托,用于接入额外服务或覆盖默认依赖绑定。 /// 是否以异步模式初始化服务模块。 /// 已绑定到当前架构类型的架构上下文。 public async Task PrepareForInitializationAsync( diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index 94acae78..d02f39e2 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -5,7 +5,7 @@ namespace GFramework.Core.Architectures; /// /// 架构模块管理器 -/// 负责管理架构模块的安装和中介行为注册 +/// 负责管理架构模块的安装和 CQRS 行为注册 /// internal sealed class ArchitectureModules( IArchitecture architecture, @@ -13,14 +13,14 @@ internal sealed class ArchitectureModules( ILogger logger) { /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 + /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 支持开放泛型行为类型和针对单一请求的封闭行为类型。 /// /// 行为类型,必须是引用类型 public void RegisterMediatorBehavior() where TBehavior : class { - logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}"); + logger.Debug($"Registering CQRS pipeline behavior: {typeof(TBehavior).Name}"); services.Container.RegisterMediatorBehavior(); } @@ -37,4 +37,4 @@ internal sealed class ArchitectureModules( logger.Info($"Module installed: {name}"); return module; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index 5c301c07..42ff1ce2 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -12,21 +12,22 @@ // limitations under the License. using GFramework.Core.Abstractions.Coroutine; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using Mediator; namespace GFramework.Core.Coroutine.Extensions; /// -/// 提供Mediator模式与协程集成的扩展方法。 -/// 包含发送命令和等待事件的协程实现。 +/// 提供 CQRS 命令与协程集成的扩展方法。 +/// 历史命名保留了 Mediator 前缀,但当前实现直接走 返回的 +/// CQRS 入口,不再依赖外部 Mediator 服务。 /// public static class MediatorCoroutineExtensions { /// - /// 以协程方式发送命令并处理可能的异常。 + /// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。 /// - /// 命令的类型 + /// 命令的类型。 /// 上下文感知对象,用于获取服务 /// 要发送的命令对象 /// 发生异常时的回调处理函数 @@ -35,13 +36,12 @@ public static class MediatorCoroutineExtensions this IContextAware contextAware, TCommand command, Action? onError = null) - where TCommand : notnull + where TCommand : IRequest { - var mediator = contextAware - .GetContext() - .GetService()!; + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); - var task = mediator.Send(command).AsTask(); + var task = contextAware.GetContext().SendAsync(command).AsTask(); yield return task.AsCoroutineInstruction(); @@ -51,4 +51,4 @@ public static class MediatorCoroutineExtensions else throw task.Exception!.InnerException ?? task.Exception; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs index 136b76a7..957c0981 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -7,7 +7,7 @@ namespace GFramework.Core.Cqrs.Internal; /// /// 在架构初始化期间扫描并注册 CQRS 处理器。 -/// 首批实现采用运行时反射扫描,优先满足“无需 AddMediator 即可工作”的迁移目标。 +/// 首批实现采用运行时反射扫描,优先满足“无需额外注册步骤即可工作”的迁移目标。 /// internal static class CqrsHandlerRegistrar { diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index dec4f267..6b45a09e 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -5,7 +5,6 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Logging; using GFramework.Core.Rule; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Ioc; @@ -310,8 +309,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 + /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 同时支持开放泛型行为类型和已闭合的具体行为类型, /// 以兼容通用行为和针对单一请求的专用行为两种注册方式。 /// @@ -351,7 +350,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } - _logger.Debug($"Mediator behavior registered: {behaviorType.Name}"); + _logger.Debug($"CQRS pipeline behavior registered: {behaviorType.Name}"); } finally { diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index d0cf0f9e..b92566d1 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -1,21 +1,21 @@ --- -title: CQRS 与 Mediator -description: CQRS 模式通过 Mediator 实现命令查询职责分离,提供清晰的业务逻辑组织方式。 +title: CQRS +description: GFramework 内建 CQRS runtime,用统一请求分发、通知发布和流式处理组织业务逻辑。 --- -# CQRS 与 Mediator +# CQRS ## 概述 CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,将数据的读取(Query)和修改(Command)操作分离。GFramework -通过集成 Mediator 库实现了 CQRS 模式,提供了类型安全、解耦的业务逻辑处理方式。 +当前内建自有 CQRS runtime,通过统一的请求分发器、通知发布和流式请求管道提供类型安全、解耦的业务逻辑处理方式。 通过 CQRS,你可以将复杂的业务逻辑拆分为独立的命令和查询处理器,每个处理器只负责单一职责,使代码更易于测试和维护。 **主要特性**: - 命令查询职责分离 -- 基于 Mediator 模式的解耦设计 +- 内建请求分发与解耦设计 - 支持管道行为(Behaviors) - 异步处理支持 - 与架构系统深度集成 @@ -72,7 +72,6 @@ public class GetPlayerQuery : QueryBase ```csharp using GFramework.Core.CQRS.Command; -using Mediator; // 命令处理器 public class CreatePlayerCommandHandler : AbstractCommandHandler @@ -92,19 +91,19 @@ public class CreatePlayerCommandHandler : AbstractCommandHandler // 4. 发送命令 public async Task SaveGame() { - var mediator = this.GetService(); - var command = new SaveGameCommand(new SaveGameInput { SlotId = 1, Data = currentGameData }); - await mediator.Send(command); + await this.SendAsync(command); } ``` @@ -195,21 +192,19 @@ public class GetHighScoresQueryHandler : AbstractQueryHandler> GetHighScores() { - var mediator = this.GetService(); - var query = new GetHighScoresQuery(new GetHighScoresInput { Count = 10 }); - var scores = await mediator.Send(query); + var scores = await this.SendQueryAsync(query); return scores; } ``` ### 注册处理器 -在架构中注册 Mediator 和处理器: +在架构中注册 CQRS 行为并让处理器自动扫描注册: ```csharp public class GameArchitecture : Architecture @@ -225,7 +220,7 @@ public class GameArchitecture : Architecture } ``` -`RegisterMediatorBehavior()` 同时支持两种形式: +`RegisterMediatorBehavior()` 是保留的兼容名称,当前用于注册框架内建 CQRS pipeline,支持两种形式: - 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 - 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` @@ -326,7 +321,7 @@ var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput NewLevel = 10 }); -await mediator.Publish(notification); +await this.PublishAsync(notification); ``` ### Pipeline Behaviors(管道行为) @@ -334,11 +329,11 @@ await mediator.Publish(notification); Behaviors 可以在处理器执行前后添加横切关注点: ```csharp -using Mediator; +using GFramework.Core.Abstractions.Cqrs; // 日志行为 public class LoggingBehavior : IPipelineBehavior - where TMessage : IMessage + where TMessage : IRequest { public async ValueTask Handle( TMessage message, @@ -358,7 +353,7 @@ public class LoggingBehavior : IPipelineBehavior : IPipelineBehavior - where TMessage : IMessage + where TMessage : IRequest { public async ValueTask Handle( TMessage message, @@ -390,7 +385,7 @@ RegisterMediatorBehavior>(); ```csharp public class ValidationBehavior : IPipelineBehavior - where TMessage : IMessage + where TMessage : IRequest { public async ValueTask Handle( TMessage message, @@ -441,7 +436,7 @@ public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler Handle(...) ### 问题:处理器可以调用其他处理器吗? **解答**: -可以,通过 Mediator 发送新的命令或查询: +可以,通过架构上下文继续发送新的命令或查询: ```csharp public override async ValueTask Handle(...) { - var mediator = this.GetService(); - // 调用其他命令 - await mediator.Send(new AnotherCommand(...)); + await this.SendAsync(new AnotherCommand(...)); return Unit.Value; } diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md index 8430e58f..98455237 100644 --- a/docs/zh-CN/core/index.md +++ b/docs/zh-CN/core/index.md @@ -391,17 +391,17 @@ public class PlayerController : IController #### 5. ArchitectureModules (模块管理器) -**职责**: 管理架构模块和中介行为 +**职责**: 管理架构模块和 CQRS 管道行为 **核心功能**: - 模块安装 (IArchitectureModule) -- 中介行为注册 (Mediator Behaviors) +- CQRS 管道行为注册(历史 API 名称仍为 `RegisterMediatorBehavior`) **关键方法**: - `InstallModule()` - 安装模块 -- `RegisterMediatorBehavior()` - 注册中介行为 +- `RegisterMediatorBehavior()` - 注册 CQRS 管道行为 #### 设计优势 @@ -672,4 +672,3 @@ public interface IController : - 添加 `PhaseChanged` 事件,支持阶段监听 **向后兼容**: 所有公共 API 保持不变,现有代码无需修改。 - From 115fe65e88599c173d8175b3e4e035c3f5d00ad0 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:54:27 +0800 Subject: [PATCH 06/70] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=92=8C=E6=A0=B8=E5=BF=83=E6=A1=86=E6=9E=B6=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 模块详细文档,介绍命令查询职责分离模式 - 添加核心框架架构概述和五层架构设计说明 - 补充快速开始指南和最佳实践建议 - 完善包说明和组件联动机制介绍 - 添加架构生命周期管理和模块化设计说明 --- .../Architectures/IArchitecture.cs | 12 +- .../Ioc/IIocContainer.cs | 10 +- .../ArchitectureModulesBehaviorTests.cs | 4 +- .../RegistryInitializationHookBaseTests.cs | 19 ++- ...sts.cs => CqrsCoroutineExtensionsTests.cs} | 12 +- GFramework.Core/Architectures/Architecture.cs | 14 +- .../Architectures/ArchitectureModules.cs | 16 ++- .../Extensions/CqrsCoroutineExtensions.cs | 43 ++++++ .../Extensions/MediatorCoroutineExtensions.cs | 18 +-- .../ContextAwareCqrsCommandExtensions.cs | 44 +++++++ .../Extensions/ContextAwareCqrsExtensions.cs | 123 ++++++++++++++++++ .../ContextAwareCqrsQueryExtensions.cs | 44 +++++++ .../ContextAwareMediatorCommandExtensions.cs | 22 ++-- .../ContextAwareMediatorExtensions.cs | 58 ++++----- .../ContextAwareMediatorQueryExtensions.cs | 22 ++-- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 14 +- .../ContextAwareCoroutineExtensions.cs | 19 ++- docs/zh-CN/core/cqrs.md | 15 ++- docs/zh-CN/core/index.md | 4 +- 19 files changed, 402 insertions(+), 111 deletions(-) rename GFramework.Core.Tests/Coroutine/{MediatorCoroutineExtensionsTests.cs => CqrsCoroutineExtensionsTests.cs} (86%) create mode 100644 GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs create mode 100644 GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs create mode 100644 GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs create mode 100644 GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index bb855189..78303a2d 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -73,11 +73,21 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// /// 注册 CQRS 请求管道行为。 - /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, /// 也支持绑定到单一请求/响应对的封闭行为类型。 /// /// 行为类型,必须是引用类型 + void RegisterCqrsPipelineBehavior() + where TBehavior : class; + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, + /// 也支持绑定到单一请求/响应对的封闭行为类型。 + /// + /// 行为类型,必须是引用类型 + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] void RegisterMediatorBehavior() where TBehavior : class; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index d00211ed..0e55908a 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -90,9 +90,17 @@ public interface IIocContainer : IContextAware /// /// 注册 CQRS 请求管道行为。 - /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// /// 行为类型,必须是引用类型 + void RegisterCqrsPipelineBehavior() + where TBehavior : class; + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// + /// 行为类型,必须是引用类型 + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] void RegisterMediatorBehavior() where TBehavior : class; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index bf6566b4..c5c0b685 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -59,10 +59,10 @@ public class ArchitectureModulesBehaviorTests /// 验证注册的 CQRS 行为会参与请求管道执行。 /// [Test] - public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request() + public async Task RegisterCqrsPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Request() { var architecture = new ModuleTestArchitecture(target => - target.RegisterMediatorBehavior>()); + target.RegisterCqrsPipelineBehavior>()); await architecture.InitializeAsync(); diff --git a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs index 4e517194..38e06805 100644 --- a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs +++ b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs @@ -5,7 +5,6 @@ using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -181,11 +180,17 @@ public class TestArchitectureWithRegistry : IArchitecture throw new NotImplementedException(); } - public void RegisterMediatorBehavior() where TBehavior : class + public void RegisterCqrsPipelineBehavior() where TBehavior : class { throw new NotImplementedException(); } + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); + } + public IArchitectureModule InstallModule(IArchitectureModule module) { throw new NotImplementedException(); @@ -306,11 +311,17 @@ public class TestArchitectureWithoutRegistry : IArchitecture throw new NotImplementedException(); } - public void RegisterMediatorBehavior() where TBehavior : class + public void RegisterCqrsPipelineBehavior() where TBehavior : class { throw new NotImplementedException(); } + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); + } + public IArchitectureModule InstallModule(IArchitectureModule module) { throw new NotImplementedException(); @@ -363,4 +374,4 @@ public class TestArchitectureWithoutRegistry : IArchitecture public void RegisterLifecycleHook(IArchitectureLifecycleHook hook) { } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs similarity index 86% rename from GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs rename to GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index 9dfa7b1e..2adba37a 100644 --- a/GFramework.Core.Tests/Coroutine/MediatorCoroutineExtensionsTests.cs +++ b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -15,17 +15,17 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Coroutine.Extensions; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Core.Tests.Coroutine; /// -/// 的单元测试类。 -/// 验证历史命名的协程扩展已切到框架内建 CQRS runtime, +/// 的单元测试类。 +/// 验证新的 CQRS 协程扩展直接走框架内建 CQRS runtime, /// 并确保协程对命令调度异常的传播行为保持稳定。 /// [TestFixture] -public class MediatorCoroutineExtensionsTests +public class CqrsCoroutineExtensionsTests { /// /// 验证SendCommandCoroutine应该返回IEnumerator @@ -40,7 +40,7 @@ public class MediatorCoroutineExtensionsTests .Setup(ctx => ctx.SendAsync(command, It.IsAny())) .Returns(ValueTask.CompletedTask); - var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command); + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); Assert.That(coroutine, Is.InstanceOf>()); } @@ -59,7 +59,7 @@ public class MediatorCoroutineExtensionsTests .Setup(ctx => ctx.SendAsync(command, It.IsAny())) .Returns(new ValueTask(Task.FromException(expectedException))); - var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command); + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); Assert.That(coroutine.MoveNext(), Is.True); var exception = Assert.Throws(() => coroutine.MoveNext()); diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index ac4fa2dc..ec571ee8 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -146,13 +146,23 @@ public abstract class Architecture : IArchitecture /// /// 注册 CQRS 请求管道行为。 - /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 可以传入开放泛型行为类型,也可以传入绑定到特定请求的封闭行为类型。 /// /// 行为类型,必须是引用类型 + public void RegisterCqrsPipelineBehavior() where TBehavior : class + { + _modules.RegisterCqrsPipelineBehavior(); + } + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// + /// 行为类型,必须是引用类型 + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] public void RegisterMediatorBehavior() where TBehavior : class { - _modules.RegisterMediatorBehavior(); + RegisterCqrsPipelineBehavior(); } /// diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index d02f39e2..7563e276 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -14,14 +14,24 @@ internal sealed class ArchitectureModules( { /// /// 注册 CQRS 请求管道行为。 - /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 支持开放泛型行为类型和针对单一请求的封闭行为类型。 /// /// 行为类型,必须是引用类型 - public void RegisterMediatorBehavior() where TBehavior : class + public void RegisterCqrsPipelineBehavior() where TBehavior : class { logger.Debug($"Registering CQRS pipeline behavior: {typeof(TBehavior).Name}"); - services.Container.RegisterMediatorBehavior(); + services.Container.RegisterCqrsPipelineBehavior(); + } + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// + /// 行为类型,必须是引用类型 + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); } /// diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs new file mode 100644 index 00000000..55e6a67f --- /dev/null +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -0,0 +1,43 @@ +using GFramework.Core.Abstractions.Coroutine; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Coroutine.Extensions; + +namespace GFramework.Core.Cqrs.Extensions; + +/// +/// 提供 CQRS 命令与协程集成的扩展方法。 +/// 这些扩展直接走架构上下文的内建 CQRS runtime,不依赖外部 Mediator 服务。 +/// +public static class CqrsCoroutineExtensions +{ + /// + /// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。 + /// + /// 命令类型。 + /// 上下文感知对象,用于获取架构上下文。 + /// 要发送的命令对象。 + /// 发生异常时的回调处理函数。 + /// 协程枚举器,用于协程执行。 + public static IEnumerator SendCommandCoroutine( + this IContextAware contextAware, + TCommand command, + Action? onError = null) + where TCommand : IRequest + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + var task = contextAware.GetContext().SendAsync(command).AsTask(); + + yield return task.AsCoroutineInstruction(); + + if (!task.IsFaulted) + yield break; + + if (onError != null) + onError.Invoke(task.Exception!); + else + throw task.Exception!.InnerException ?? task.Exception; + } +} diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index 42ff1ce2..91ed0238 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -14,14 +14,15 @@ using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 -/// 历史命名保留了 Mediator 前缀,但当前实现直接走 返回的 -/// CQRS 入口,不再依赖外部 Mediator 服务。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// +[Obsolete("Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead.")] public static class MediatorCoroutineExtensions { /// @@ -38,17 +39,6 @@ public static class MediatorCoroutineExtensions Action? onError = null) where TCommand : IRequest { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var task = contextAware.GetContext().SendAsync(command).AsTask(); - - yield return task.AsCoroutineInstruction(); - - if (!task.IsFaulted) yield break; - if (onError != null) - onError.Invoke(task.Exception!); - else - throw task.Exception!.InnerException ?? task.Exception; + return CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command, onError); } } diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs new file mode 100644 index 00000000..5f86f4e5 --- /dev/null +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -0,0 +1,44 @@ +using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Core.Cqrs.Extensions; + +/// +/// 提供对 接口的 CQRS 命令扩展方法。 +/// +public static class ContextAwareCqrsCommandExtensions +{ + /// + /// 发送命令的同步版本(不推荐,仅用于兼容同步调用链)。 + /// + /// 命令响应类型。 + /// 实现 接口的对象。 + /// 要发送的命令对象。 + /// 命令执行结果。 + public static TResponse SendCommand(this IContextAware contextAware, ICommand command) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendCommand(command); + } + + /// + /// 异步发送命令并返回结果。 + /// + /// 命令响应类型。 + /// 实现 接口的对象。 + /// 要发送的命令对象。 + /// 取消令牌,用于取消操作。 + /// 包含命令执行结果的 + public static ValueTask SendCommandAsync( + this IContextAware contextAware, + ICommand command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendCommandAsync(command, cancellationToken); + } +} diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs new file mode 100644 index 00000000..64f9d0e1 --- /dev/null +++ b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs @@ -0,0 +1,123 @@ +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Core.Cqrs.Extensions; + +/// +/// 提供对 接口的 CQRS 统一扩展方法。 +/// 这些扩展直接委托给架构上下文的内建 CQRS runtime,作为新的中性命名入口。 +/// +public static class ContextAwareCqrsExtensions +{ + /// + /// 发送请求(统一处理 Command/Query)。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求结果。 + public static ValueTask SendRequestAsync( + this IContextAware contextAware, + IRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(request); + + return contextAware.GetContext().SendRequestAsync(request, cancellationToken); + } + + /// + /// 发送请求(同步版本,不推荐)。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 要发送的请求。 + /// 请求结果。 + public static TResponse SendRequest(this IContextAware contextAware, IRequest request) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(request); + + return contextAware.GetContext().SendRequest(request); + } + + /// + /// 发布通知(一对多事件)。 + /// + /// 通知类型。 + /// 实现 接口的对象。 + /// 要发布的通知。 + /// 取消令牌。 + /// 异步任务。 + public static ValueTask PublishAsync( + this IContextAware contextAware, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(notification); + + return contextAware.GetContext().PublishAsync(notification, cancellationToken); + } + + /// + /// 创建流式请求。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 流式请求。 + /// 取消令牌。 + /// 异步响应流。 + public static IAsyncEnumerable CreateStream( + this IContextAware contextAware, + IStreamRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(request); + + return contextAware.GetContext().CreateStream(request, cancellationToken); + } + + /// + /// 发送无返回值命令。 + /// + /// 命令类型。 + /// 实现 接口的对象。 + /// 要发送的命令。 + /// 取消令牌。 + /// 异步任务。 + public static ValueTask SendAsync( + this IContextAware contextAware, + TCommand command, + CancellationToken cancellationToken = default) + where TCommand : IRequest + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendAsync(command, cancellationToken); + } + + /// + /// 发送带返回值命令。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令执行结果。 + public static ValueTask SendAsync( + this IContextAware contextAware, + IRequest command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendAsync(command, cancellationToken); + } +} diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs new file mode 100644 index 00000000..42905215 --- /dev/null +++ b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -0,0 +1,44 @@ +using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Core.Cqrs.Extensions; + +/// +/// 提供对 接口的 CQRS 查询扩展方法。 +/// +public static class ContextAwareCqrsQueryExtensions +{ + /// + /// 发送查询的同步版本(不推荐,仅用于兼容同步调用链)。 + /// + /// 查询响应类型。 + /// 实现 接口的对象。 + /// 要发送的查询对象。 + /// 查询结果。 + public static TResponse SendQuery(this IContextAware contextAware, IQuery query) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(query); + + return contextAware.GetContext().SendQuery(query); + } + + /// + /// 异步发送查询并返回结果。 + /// + /// 查询响应类型。 + /// 实现 接口的对象。 + /// 要发送的查询对象。 + /// 取消令牌,用于取消操作。 + /// 包含查询结果的 + public static ValueTask SendQueryAsync( + this IContextAware contextAware, + IQuery query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(query); + + return contextAware.GetContext().SendQueryAsync(query, cancellationToken); + } +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 881741e7..9fc9311b 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,11 +1,14 @@ -using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Core.Extensions; /// -/// 提供对 IContextAware 接口的 CQRS 命令扩展方法。 +/// 提供对 接口的 CQRS 命令扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// +[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.")] public static class ContextAwareMediatorCommandExtensions { /// @@ -19,11 +22,7 @@ public static class ContextAwareMediatorCommandExtensions public static TResponse SendCommand(this IContextAware contextAware, ICommand command) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var context = contextAware.GetContext(); - return context.SendCommand(command); + return ContextAwareCqrsCommandExtensions.SendCommand(contextAware, command); } /// @@ -38,10 +37,9 @@ public static class ContextAwareMediatorCommandExtensions public static ValueTask SendCommandAsync(this IContextAware contextAware, ICommand command, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var context = contextAware.GetContext(); - return context.SendCommandAsync(command, cancellationToken); + return ContextAwareCqrsCommandExtensions.SendCommandAsync( + contextAware, + command, + cancellationToken); } } diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index 1661d87b..4b63e223 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,11 +1,14 @@ -using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Core.Extensions; /// -/// 提供对 IContextAware 接口的 CQRS 统一接口扩展方法。 +/// 提供对 接口的 CQRS 统一接口扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// +[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead.")] public static class ContextAwareMediatorExtensions { /// @@ -20,11 +23,10 @@ public static class ContextAwareMediatorExtensions public static ValueTask SendRequestAsync(this IContextAware contextAware, IRequest request, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(request); - - var context = contextAware.GetContext(); - return context.SendRequestAsync(request, cancellationToken); + return ContextAwareCqrsExtensions.SendRequestAsync( + contextAware, + request, + cancellationToken); } /// @@ -38,11 +40,7 @@ public static class ContextAwareMediatorExtensions public static TResponse SendRequest(this IContextAware contextAware, IRequest request) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(request); - - var context = contextAware.GetContext(); - return context.SendRequest(request); + return ContextAwareCqrsExtensions.SendRequest(contextAware, request); } /// @@ -58,11 +56,10 @@ public static class ContextAwareMediatorExtensions TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(notification); - - var context = contextAware.GetContext(); - return context.PublishAsync(notification, cancellationToken); + return ContextAwareCqrsExtensions.PublishAsync( + contextAware, + notification, + cancellationToken); } /// @@ -77,11 +74,10 @@ public static class ContextAwareMediatorExtensions public static IAsyncEnumerable CreateStream(this IContextAware contextAware, IStreamRequest request, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(request); - - var context = contextAware.GetContext(); - return context.CreateStream(request, cancellationToken); + return ContextAwareCqrsExtensions.CreateStream( + contextAware, + request, + cancellationToken); } /// @@ -97,11 +93,10 @@ public static class ContextAwareMediatorExtensions CancellationToken cancellationToken = default) where TCommand : IRequest { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var context = contextAware.GetContext(); - return context.SendAsync(command, cancellationToken); + return ContextAwareCqrsExtensions.SendAsync( + contextAware, + command, + cancellationToken); } /// @@ -116,10 +111,9 @@ public static class ContextAwareMediatorExtensions public static ValueTask SendAsync(this IContextAware contextAware, IRequest command, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var context = contextAware.GetContext(); - return context.SendAsync(command, cancellationToken); + return ContextAwareCqrsExtensions.SendAsync( + contextAware, + command, + cancellationToken); } } diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index 5a4bfeb6..be2d70e1 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,11 +1,14 @@ -using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Core.Extensions; /// -/// 提供对 IContextAware 接口的 CQRS 查询扩展方法。 +/// 提供对 接口的 CQRS 查询扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// +[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.")] public static class ContextAwareMediatorQueryExtensions { /// @@ -18,11 +21,7 @@ public static class ContextAwareMediatorQueryExtensions /// 当 contextAware 或 query 为 null 时抛出 public static TResponse SendQuery(this IContextAware contextAware, IQuery query) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(query); - - var context = contextAware.GetContext(); - return context.SendQuery(query); + return ContextAwareCqrsQueryExtensions.SendQuery(contextAware, query); } /// @@ -37,10 +36,9 @@ public static class ContextAwareMediatorQueryExtensions public static ValueTask SendQueryAsync(this IContextAware contextAware, IQuery query, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(query); - - var context = contextAware.GetContext(); - return context.SendQueryAsync(query, cancellationToken); + return ContextAwareCqrsQueryExtensions.SendQueryAsync( + contextAware, + query, + cancellationToken); } } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 6b45a09e..69b46720 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -310,12 +310,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// /// 注册 CQRS 请求管道行为。 - /// 历史方法名保留了 Mediator 前缀,但当前用于配置框架内建 CQRS runtime 的行为拦截和处理逻辑。 /// 同时支持开放泛型行为类型和已闭合的具体行为类型, /// 以兼容通用行为和针对单一请求的专用行为两种注册方式。 /// /// 行为类型,必须是引用类型 - public void RegisterMediatorBehavior() where TBehavior : class + public void RegisterCqrsPipelineBehavior() where TBehavior : class { _lock.EnterWriteLock(); try @@ -358,6 +357,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// + /// 行为类型,必须是引用类型 + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); + } + /// /// 配置服务 /// diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index d97d08a7..5d1a3485 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -2,9 +2,6 @@ using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Coroutine; -using GFramework.Core.Coroutine.Extensions; -using GFramework.Core.Extensions; namespace GFramework.Godot.Coroutine; @@ -29,8 +26,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .SendCommandAsync(command, cancellationToken) + return Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions + .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); @@ -53,8 +50,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .SendCommandAsync(command, cancellationToken) + return Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions + .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); @@ -77,8 +74,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .SendQueryAsync(query, cancellationToken) + return Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions + .SendQueryAsync(contextAware, query, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); @@ -100,8 +97,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .PublishAsync(notification, cancellationToken) + return Core.Cqrs.Extensions.ContextAwareCqrsExtensions + .PublishAsync(contextAware, notification, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index b92566d1..742d2ef8 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -212,15 +212,16 @@ public class GameArchitecture : Architecture protected override void Init() { // 注册通用开放泛型行为 - RegisterMediatorBehavior>(); - RegisterMediatorBehavior>(); + RegisterCqrsPipelineBehavior>(); + RegisterCqrsPipelineBehavior>(); // 处理器会自动通过依赖注入注册 } } ``` -`RegisterMediatorBehavior()` 是保留的兼容名称,当前用于注册框架内建 CQRS pipeline,支持两种形式: +`RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` +仅作为兼容名称保留。当前接口支持两种形式: - 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 - 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` @@ -377,8 +378,8 @@ public class PerformanceBehavior : IPipelineBehavior>(); -RegisterMediatorBehavior>(); +RegisterCqrsPipelineBehavior>(); +RegisterCqrsPipelineBehavior>(); ``` ### 验证行为 @@ -471,8 +472,8 @@ await foreach (var player in stream) 4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等 ```csharp - RegisterMediatorBehavior>(); - RegisterMediatorBehavior>(); + RegisterCqrsPipelineBehavior>(); + RegisterCqrsPipelineBehavior>(); ``` 5. **保持处理器简单**:一个处理器只做一件事 diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md index 98455237..5c4837bf 100644 --- a/docs/zh-CN/core/index.md +++ b/docs/zh-CN/core/index.md @@ -396,12 +396,12 @@ public class PlayerController : IController **核心功能**: - 模块安装 (IArchitectureModule) -- CQRS 管道行为注册(历史 API 名称仍为 `RegisterMediatorBehavior`) +- CQRS 管道行为注册(推荐 API 为 `RegisterCqrsPipelineBehavior`) **关键方法**: - `InstallModule()` - 安装模块 -- `RegisterMediatorBehavior()` - 注册 CQRS 管道行为 +- `RegisterCqrsPipelineBehavior()` - 注册 CQRS 管道行为 #### 设计优势 From 088f02d586a9cff9fcbde478f64cb1e16f957d17 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:34:01 +0800 Subject: [PATCH 07/70] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E6=96=87=E6=A1=A3=E5=B9=B6=E5=AE=8C=E5=96=84=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=89=A9=E5=B1=95=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 核心概念、命令查询处理器使用指南 - 添加管道行为、流式处理和最佳实践说明 - 实现 CQRS 协程扩展方法支持异步命令执行 - 添加 ContextAware 接口的 CQRS 命令查询扩展 - 集成 Microsoft DI 容器依赖注入支持 - 补充架构模块行为测试验证功能完整性 - 扩展 GameContext 测试用例提高代码覆盖率 --- .../Architectures/IArchitecture.cs | 1 + .../Ioc/IIocContainer.cs | 1 + .../ArchitectureModulesBehaviorTests.cs | 22 ++++++ .../Architectures/GameContextTests.cs | 77 +++++++++++++++++++ .../Coroutine/CqrsCoroutineExtensionsTests.cs | 26 +++++++ GFramework.Core.Tests/GlobalUsings.cs | 4 +- .../Extensions/CqrsCoroutineExtensions.cs | 12 ++- .../ContextAwareCqrsCommandExtensions.cs | 6 ++ .../Extensions/ContextAwareCqrsExtensions.cs | 18 +++++ .../ContextAwareCqrsQueryExtensions.cs | 6 ++ GFramework.Core/GlobalUsings.cs | 3 +- .../ContextAwareCoroutineExtensions.cs | 11 ++- docs/zh-CN/core/cqrs.md | 20 ++--- 13 files changed, 190 insertions(+), 17 deletions(-) diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index 78303a2d..b344717d 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -2,6 +2,7 @@ using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; +using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Architectures; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 0e55908a..c56b71fa 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -1,5 +1,6 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; +using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Ioc; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index c5c0b685..ade4a1a7 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -77,6 +77,28 @@ public class ArchitectureModulesBehaviorTests await architecture.DestroyAsync(); } + /// + /// 验证兼容别名 RegisterMediatorBehavior 仍会把 CQRS 行为接入请求管道。 + /// + [Test] + public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request() + { + var architecture = new ModuleTestArchitecture(target => + target.RegisterMediatorBehavior>()); + + await architecture.InitializeAsync(); + + var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); + + Assert.Multiple(() => + { + Assert.That(response, Is.EqualTo("handled")); + Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1)); + }); + + await architecture.DestroyAsync(); + } + /// /// 用于测试模块行为的最小架构实现。 /// diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index e11f0892..28e3cc97 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -394,45 +394,106 @@ public class TestArchitectureContext : IArchitectureContext { } + /// + /// 测试桩:异步发送统一 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendRequestAsync(IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送统一 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 请求响应。 + /// 该测试桩未实现此成员。 public TResponse SendRequest(IRequest request) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 命令响应。 + /// 该测试桩未实现此成员。 public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 取消令牌。 + /// 查询结果任务。 + /// 该测试桩未实现此成员。 public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 查询结果。 + /// 该测试桩未实现此成员。 public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发布 CQRS 通知。 + /// + /// 通知类型。 + /// 要发布的通知。 + /// 取消令牌。 + /// 通知发布任务。 + /// 该测试桩未实现此成员。 public ValueTask PublishAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification { throw new NotImplementedException(); } + /// + /// 测试桩:创建 CQRS 流式请求响应序列。 + /// + /// 流式响应元素类型。 + /// 流式请求。 + /// 取消令牌。 + /// 异步响应流。 + /// 该测试桩未实现此成员。 public IAsyncEnumerable CreateStream( IStreamRequest request, CancellationToken cancellationToken = default) @@ -440,12 +501,28 @@ public class TestArchitectureContext : IArchitectureContext throw new NotImplementedException(); } + /// + /// 测试桩:异步发送无返回值 CQRS 命令。 + /// + /// 命令类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令发送任务。 + /// 该测试桩未实现此成员。 public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : IRequest { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送带返回值的 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendAsync(IRequest command, CancellationToken cancellationToken = default) { diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index 2adba37a..d5f85f1d 100644 --- a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -66,6 +66,32 @@ public class CqrsCoroutineExtensionsTests Assert.That(exception, Is.SameAs(expectedException)); } + /// + /// 验证 SendCommandCoroutine 在提供错误回调时也会传递解包后的原始异常, + /// 避免回调路径暴露 。 + /// + [Test] + public void SendCommandCoroutine_Should_Forward_Inner_Exception_To_Error_Handler() + { + var command = new TestCommand("Test"); + var contextAware = new TestContextAware(); + var expectedException = new InvalidOperationException("Command failed."); + Exception? capturedException = null; + + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromException(expectedException))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine( + contextAware, + command, + exception => capturedException = exception); + + Assert.That(coroutine.MoveNext(), Is.True); + Assert.That(coroutine.MoveNext(), Is.False); + Assert.That(capturedException, Is.SameAs(expectedException)); + } + /// /// 测试用的简单命令类 /// diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index 18957f6b..fe9b7de1 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -23,4 +23,6 @@ global using GFramework.Core.Abstractions.StateManagement; global using GFramework.Core.Extensions; global using GFramework.Core.Property; global using GFramework.Core.StateManagement; -global using GFramework.Core.Abstractions.Property; \ No newline at end of file +global using GFramework.Core.Abstractions.Property; +global using Microsoft.Extensions.DependencyInjection; +global using Moq; diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 55e6a67f..40367ff8 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -19,6 +19,13 @@ public static class CqrsCoroutineExtensions /// 要发送的命令对象。 /// 发生异常时的回调处理函数。 /// 协程枚举器,用于协程执行。 + /// + /// 当 时抛出。 + /// + /// + /// 当底层命令调度失败时,该扩展会把底层异常解包后传给 , + /// 或在未提供回调时重新抛出同一个异常实例,避免两条失败路径暴露不同的异常类型。 + /// public static IEnumerator SendCommandCoroutine( this IContextAware contextAware, TCommand command, @@ -35,9 +42,10 @@ public static class CqrsCoroutineExtensions if (!task.IsFaulted) yield break; + var exception = task.Exception!.InnerException ?? task.Exception; if (onError != null) - onError.Invoke(task.Exception!); + onError.Invoke(exception); else - throw task.Exception!.InnerException ?? task.Exception; + throw exception; } } diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs index 5f86f4e5..233228ed 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -15,6 +15,9 @@ public static class ContextAwareCqrsCommandExtensions /// 实现 接口的对象。 /// 要发送的命令对象。 /// 命令执行结果。 + /// + /// 当 时抛出。 + /// public static TResponse SendCommand(this IContextAware contextAware, ICommand command) { ArgumentNullException.ThrowIfNull(contextAware); @@ -31,6 +34,9 @@ public static class ContextAwareCqrsCommandExtensions /// 要发送的命令对象。 /// 取消令牌,用于取消操作。 /// 包含命令执行结果的 + /// + /// 当 时抛出。 + /// public static ValueTask SendCommandAsync( this IContextAware contextAware, ICommand command, diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs index 64f9d0e1..ab09e689 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs @@ -17,6 +17,9 @@ public static class ContextAwareCqrsExtensions /// 要发送的请求。 /// 取消令牌。 /// 请求结果。 + /// + /// 当 时抛出。 + /// public static ValueTask SendRequestAsync( this IContextAware contextAware, IRequest request, @@ -35,6 +38,9 @@ public static class ContextAwareCqrsExtensions /// 实现 接口的对象。 /// 要发送的请求。 /// 请求结果。 + /// + /// 当 时抛出。 + /// public static TResponse SendRequest(this IContextAware contextAware, IRequest request) { ArgumentNullException.ThrowIfNull(contextAware); @@ -51,6 +57,9 @@ public static class ContextAwareCqrsExtensions /// 要发布的通知。 /// 取消令牌。 /// 异步任务。 + /// + /// 当 时抛出。 + /// public static ValueTask PublishAsync( this IContextAware contextAware, TNotification notification, @@ -71,6 +80,9 @@ public static class ContextAwareCqrsExtensions /// 流式请求。 /// 取消令牌。 /// 异步响应流。 + /// + /// 当 时抛出。 + /// public static IAsyncEnumerable CreateStream( this IContextAware contextAware, IStreamRequest request, @@ -90,6 +102,9 @@ public static class ContextAwareCqrsExtensions /// 要发送的命令。 /// 取消令牌。 /// 异步任务。 + /// + /// 当 时抛出。 + /// public static ValueTask SendAsync( this IContextAware contextAware, TCommand command, @@ -110,6 +125,9 @@ public static class ContextAwareCqrsExtensions /// 要发送的命令。 /// 取消令牌。 /// 命令执行结果。 + /// + /// 当 时抛出。 + /// public static ValueTask SendAsync( this IContextAware contextAware, IRequest command, diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs index 42905215..9906bc3d 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -15,6 +15,9 @@ public static class ContextAwareCqrsQueryExtensions /// 实现 接口的对象。 /// 要发送的查询对象。 /// 查询结果。 + /// + /// 当 时抛出。 + /// public static TResponse SendQuery(this IContextAware contextAware, IQuery query) { ArgumentNullException.ThrowIfNull(contextAware); @@ -31,6 +34,9 @@ public static class ContextAwareCqrsQueryExtensions /// 要发送的查询对象。 /// 取消令牌,用于取消操作。 /// 包含查询结果的 + /// + /// 当 时抛出。 + /// public static ValueTask SendQueryAsync( this IContextAware contextAware, IQuery query, diff --git a/GFramework.Core/GlobalUsings.cs b/GFramework.Core/GlobalUsings.cs index 8add267e..203366e6 100644 --- a/GFramework.Core/GlobalUsings.cs +++ b/GFramework.Core/GlobalUsings.cs @@ -16,4 +16,5 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; -global using System.Threading.Channels; \ No newline at end of file +global using System.Threading.Channels; +global using Microsoft.Extensions.DependencyInjection; diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index 5d1a3485..98d57936 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -2,6 +2,9 @@ using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Coroutine; +using GFramework.Core.Coroutine.Extensions; +using GFramework.Core.Cqrs.Extensions; namespace GFramework.Godot.Coroutine; @@ -26,7 +29,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions + return ContextAwareCqrsCommandExtensions .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() @@ -50,7 +53,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions + return ContextAwareCqrsCommandExtensions .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() @@ -74,7 +77,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions + return ContextAwareCqrsQueryExtensions .SendQueryAsync(contextAware, query, cancellationToken) .AsTask() .ToCoroutineEnumerator() @@ -97,7 +100,7 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return Core.Cqrs.Extensions.ContextAwareCqrsExtensions + return ContextAwareCqrsExtensions .PublishAsync(contextAware, notification, cancellationToken) .AsTask() .ToCoroutineEnumerator() diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 742d2ef8..3effaa86 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -204,22 +204,24 @@ public async Task> GetHighScores() ### 注册处理器 -在架构中注册 CQRS 行为并让处理器自动扫描注册: +在架构中注册 CQRS 行为;默认会自动扫描当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: ```csharp public class GameArchitecture : Architecture { - protected override void Init() + protected override void OnInitialize() { // 注册通用开放泛型行为 RegisterCqrsPipelineBehavior>(); RegisterCqrsPipelineBehavior>(); - // 处理器会自动通过依赖注入注册 + // 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器 } } ``` +如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。 + `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` 仅作为兼容名称保留。当前接口支持两种形式: @@ -338,8 +340,8 @@ public class LoggingBehavior : IPipelineBehavior Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { var messageName = message.GetType().Name; Console.WriteLine($"[开始] {messageName}"); @@ -358,8 +360,8 @@ public class PerformanceBehavior : IPipelineBehavior Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); @@ -390,8 +392,8 @@ public class ValidationBehavior : IPipelineBehavior Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { // 验证输入 if (message is IValidatable validatable) From 5a2981a5577740e8cf61bad8bec98636913e7196 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:18:27 +0800 Subject: [PATCH 08/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E5=8D=8F=E7=A8=8B=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsCoroutineExtensions 扩展类,提供协程方式发送 CQRS 命令的功能 - 添加 SendCommandCoroutine 方法支持命令异步执行与异常处理 - 实现取消操作的特殊处理逻辑,区分取消、失败和成功状态 - 添加 ContextAwareCqrsCommandExtensions 扩展类,提供同步和异步命令发送方法 - 增加对 TaskCanceledException 的专门处理机制 - 完善相关单元测试,验证取消操作的异常处理行为 --- .../Coroutine/CqrsCoroutineExtensionsTests.cs | 58 +++++++++++++++++++ .../Extensions/CqrsCoroutineExtensions.cs | 20 ++++++- .../ContextAwareCqrsCommandExtensions.cs | 9 +++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index d5f85f1d..67e9537d 100644 --- a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -92,6 +92,53 @@ public class CqrsCoroutineExtensionsTests Assert.That(capturedException, Is.SameAs(expectedException)); } + /// + /// 验证 SendCommandCoroutine 在底层命令被取消且未提供错误回调时会抛出取消异常。 + /// + [Test] + public void SendCommandCoroutine_Should_Throw_TaskCanceledException_When_Command_Is_Canceled() + { + var command = new TestCommand("Test"); + var contextAware = new TestContextAware(); + using var cancellationTokenSource = new CancellationTokenSource(); + + cancellationTokenSource.Cancel(); + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); + + Assert.That(coroutine.MoveNext(), Is.True); + Assert.Throws(() => coroutine.MoveNext()); + } + + /// + /// 验证 SendCommandCoroutine 在底层命令被取消且提供错误回调时会把取消异常转发给回调。 + /// + [Test] + public void SendCommandCoroutine_Should_Forward_TaskCanceledException_To_Error_Handler_When_Command_Is_Canceled() + { + var command = new TestCommand("Test"); + var contextAware = new TestContextAware(); + using var cancellationTokenSource = new CancellationTokenSource(); + Exception? capturedException = null; + + cancellationTokenSource.Cancel(); + contextAware.MockContext + .Setup(ctx => ctx.SendAsync(command, It.IsAny())) + .Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine( + contextAware, + command, + exception => capturedException = exception); + + Assert.That(coroutine.MoveNext(), Is.True); + Assert.That(coroutine.MoveNext(), Is.False); + Assert.That(capturedException, Is.TypeOf()); + } + /// /// 测试用的简单命令类 /// @@ -102,13 +149,24 @@ public class CqrsCoroutineExtensionsTests /// private sealed class TestContextAware : IContextAware { + /// + /// 提供可配置的架构上下文 Mock。 + /// public Mock MockContext { get; } = new(); + /// + /// 获取当前架构上下文。 + /// + /// 用于 CQRS 调用的架构上下文实例。 public IArchitectureContext GetContext() { return MockContext.Object; } + /// + /// 设置架构上下文。 + /// + /// 要设置的架构上下文。 public void SetContext(IArchitectureContext context) { } diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 40367ff8..74782793 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -1,3 +1,4 @@ +using System.Runtime.ExceptionServices; using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; @@ -22,9 +23,12 @@ public static class CqrsCoroutineExtensions /// /// 当 时抛出。 /// + /// + /// 当底层命令调度被取消且未提供 时抛出。 + /// /// /// 当底层命令调度失败时,该扩展会把底层异常解包后传给 , - /// 或在未提供回调时重新抛出同一个异常实例,避免两条失败路径暴露不同的异常类型。 + /// 在取消时则统一暴露 ,避免成功、失败与取消三种完成状态被混淆。 /// public static IEnumerator SendCommandCoroutine( this IContextAware contextAware, @@ -39,6 +43,18 @@ public static class CqrsCoroutineExtensions yield return task.AsCoroutineInstruction(); + if (task.IsCanceled) + { + var canceledException = new TaskCanceledException(task); + if (onError != null) + { + onError.Invoke(canceledException); + yield break; + } + + ExceptionDispatchInfo.Capture(canceledException).Throw(); + } + if (!task.IsFaulted) yield break; @@ -46,6 +62,6 @@ public static class CqrsCoroutineExtensions if (onError != null) onError.Invoke(exception); else - throw exception; + ExceptionDispatchInfo.Capture(exception).Throw(); } } diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs index 233228ed..b71669ee 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -6,6 +6,9 @@ namespace GFramework.Core.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 /// +/// +/// 该扩展类将命令分发统一路由到架构上下文中的 CQRS 运行时。 +/// public static class ContextAwareCqrsCommandExtensions { /// @@ -18,6 +21,9 @@ public static class ContextAwareCqrsCommandExtensions /// /// 当 时抛出。 /// + /// + /// 同步方法仅用于兼容同步调用链;新代码建议优先使用异步版本。 + /// public static TResponse SendCommand(this IContextAware contextAware, ICommand command) { ArgumentNullException.ThrowIfNull(contextAware); @@ -37,6 +43,9 @@ public static class ContextAwareCqrsCommandExtensions /// /// 当 时抛出。 /// + /// + /// 该方法直接返回底层 ,避免额外的 async 状态机分配。 + /// public static ValueTask SendCommandAsync( this IContextAware contextAware, ICommand command, From 96ffd49b319a04ee4645a9b22ea522630ca9beea Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:24:48 +0800 Subject: [PATCH 09/70] =?UTF-8?q?fix(coroutine):=20=E6=9B=B4=E6=96=B0CQRS?= =?UTF-8?q?=E5=8D=8F=E7=A8=8B=E6=89=A9=E5=B1=95=E7=9A=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将TaskCanceledException更改为Exception以反映实际抛出的异常类型 - 更新异常描述以准确说明在未提供onError时的行为 - 修正文档以反映底层原始异常的传递机制 --- .../Coroutine/Extensions/CqrsCoroutineExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 74782793..b459d7f2 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -23,8 +23,8 @@ public static class CqrsCoroutineExtensions /// /// 当 时抛出。 /// - /// - /// 当底层命令调度被取消且未提供 时抛出。 + /// + /// 当底层命令调度失败且未提供 时,抛出底层原始异常。 /// /// /// 当底层命令调度失败时,该扩展会把底层异常解包后传给 , From 4c0a99d24c5d49aa7f400d6b27a2954381994f19 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:25:52 +0800 Subject: [PATCH 10/70] =?UTF-8?q?fix(coroutine):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8D=8F=E7=A8=8B=E6=89=A9=E5=B1=95=E4=B8=AD=E7=9A=84=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 TaskCanceledException 映射以统一取消状态处理 - 保留原始异常调用栈以避免调试时丢失异常来源 - 优先解包业务异常以避免直接暴露 AggregateException - 使用 ExceptionDispatchInfo.Capture 确保异常栈信息完整 --- .../Coroutine/Extensions/CqrsCoroutineExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index b459d7f2..41e54ee2 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -45,6 +45,7 @@ public static class CqrsCoroutineExtensions if (task.IsCanceled) { + // 取消态与成功态区分:协程层统一映射为 TaskCanceledException。 var canceledException = new TaskCanceledException(task); if (onError != null) { @@ -52,16 +53,18 @@ public static class CqrsCoroutineExtensions yield break; } + // 保留原始抛出栈,避免调试时丢失异常来源。 ExceptionDispatchInfo.Capture(canceledException).Throw(); } if (!task.IsFaulted) yield break; - + // 优先解包业务异常,避免直接暴露 AggregateException。 var exception = task.Exception!.InnerException ?? task.Exception; if (onError != null) onError.Invoke(exception); else + // 继续保留原始栈信息。 ExceptionDispatchInfo.Capture(exception).Throw(); } } From c0e2e9a6405bf986a0ccb131ebe506d0c2d1c0a5 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:05:22 +0800 Subject: [PATCH 11/70] =?UTF-8?q?docs(coroutine):=20=E6=9B=B4=E6=96=B0Cqrs?= =?UTF-8?q?CoroutineExtensions=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了TaskCanceledException异常说明文档 - 详细描述了命令调度取消时的异常情况 - 补充了底层命令调度相关的异常处理说明 --- .../Coroutine/Extensions/CqrsCoroutineExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 41e54ee2..98667656 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -23,6 +23,9 @@ public static class CqrsCoroutineExtensions /// /// 当 时抛出。 /// + /// + /// 当底层命令调度被取消且未提供 时抛出。 + /// /// /// 当底层命令调度失败且未提供 时,抛出底层原始异常。 /// From 295496e90f04c6eae484c20e794bbcd01ca868f4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:49:26 +0800 Subject: [PATCH 12/70] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E6=96=87=E6=A1=A3=E5=B9=B6=E5=AE=9E=E7=8E=B0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E6=A8=A1=E5=9D=97=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加完整的 CQRS 中文文档,涵盖命令、查询、处理器、管道行为等核心概念 - 实现 ArchitectureModules 类用于管理架构模块安装和 CQRS 行为注册 - 重构 Architecture 类为协调器模式,委托给专门的管理器组件 - 添加 RegisterCqrsPipelineBehavior 方法替代旧的 RegisterMediatorBehavior - 标记旧的扩展方法为 Obsolete 并提供新的兼容性别名 - 实现模块化架构组件注册和生命周期管理功能 --- CLAUDE.md | 3 +- .../Architectures/IArchitecture.cs | 6 +- .../Ioc/IIocContainer.cs | 8 +- .../MediatorCompatibilityDeprecationTests.cs | 99 +++++++++++++++++++ GFramework.Core/Architectures/Architecture.cs | 6 +- .../Architectures/ArchitectureModules.cs | 6 +- .../Extensions/MediatorCoroutineExtensions.cs | 6 +- .../ContextAwareMediatorCommandExtensions.cs | 6 +- .../ContextAwareMediatorExtensions.cs | 6 +- .../ContextAwareMediatorQueryExtensions.cs | 6 +- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 6 +- docs/zh-CN/core/cqrs.md | 3 +- 12 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 1962098f..e2e656c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,8 @@ Architecture 负责统一生命周期编排,核心阶段包括: ### CQRS 命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史 -`Mediator` 命名以兼容旧调用点。 +`Mediator` 命名以兼容旧调用点,但这些别名已进入正式弃用周期:新代码应使用 `Cqrs` 命名入口,旧别名会继续兼容一段时间并计划在未来 +major 版本中移除。 ### EventBus diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index b344717d..16386d5a 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; @@ -84,11 +85,14 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, /// 也支持绑定到单一请求/响应对的封闭行为类型。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] void RegisterMediatorBehavior() where TBehavior : class; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index c56b71fa..aead5a9d 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -1,4 +1,5 @@ -using GFramework.Core.Abstractions.Rule; +using System.ComponentModel; +using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; using Microsoft.Extensions.DependencyInjection; @@ -99,9 +100,12 @@ public interface IIocContainer : IContextAware /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] void RegisterMediatorBehavior() where TBehavior : class; diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs new file mode 100644 index 00000000..2e3bdbca --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -0,0 +1,99 @@ +using System.ComponentModel; +using System.Reflection; +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Architectures; +using GFramework.Core.Coroutine.Extensions; +using GFramework.Core.Ioc; + +namespace GFramework.Core.Tests.Cqrs; + +/// +/// 锁定历史 Mediator 兼容入口的正式弃用策略。 +/// 这些测试确保旧 API 不仅保留行为兼容,还会通过编译期提示和 IntelliSense 隐藏引导调用方迁移到新的 CQRS 命名。 +/// +[TestFixture] +public class MediatorCompatibilityDeprecationTests +{ + /// + /// 验证公开兼容方法仍可用,但已被显式标记为未来移除的旧别名。 + /// + [Test] + public void Legacy_Public_Methods_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing() + { + AssertLegacyMethod(typeof(IArchitecture), nameof(IArchitecture.RegisterMediatorBehavior)); + AssertLegacyMethod(typeof(IIocContainer), nameof(IIocContainer.RegisterMediatorBehavior)); + AssertLegacyMethod(typeof(Architecture), nameof(Architecture.RegisterMediatorBehavior)); + AssertLegacyMethod(typeof(MicrosoftDiContainer), nameof(MicrosoftDiContainer.RegisterMediatorBehavior)); + } + + /// + /// 验证历史扩展类型会把迁移目标写入弃用说明,并从 IntelliSense 主路径隐藏。 + /// + [Test] + public void Legacy_Extension_Types_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing() + { + AssertLegacyType( + typeof(ContextAwareMediatorExtensions), + "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead."); + AssertLegacyType( + typeof(ContextAwareMediatorCommandExtensions), + "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead."); + AssertLegacyType( + typeof(ContextAwareMediatorQueryExtensions), + "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead."); + AssertLegacyType( + typeof(MediatorCoroutineExtensions), + "Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead."); + } + + /// + /// 断言方法级兼容 API 具备统一的弃用元数据。 + /// + /// 声明该方法的类型。 + /// 方法名称。 + private static void AssertLegacyMethod(Type declaringType, string methodName) + { + var method = declaringType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Single(candidate => candidate.Name == methodName); + + var obsoleteAttribute = method.GetCustomAttribute(); + var editorBrowsableAttribute = method.GetCustomAttribute(); + + Assert.Multiple(() => + { + Assert.That(obsoleteAttribute, Is.Not.Null); + Assert.That( + obsoleteAttribute!.Message, + Does.Contain("Use RegisterCqrsPipelineBehavior() instead.")); + Assert.That( + obsoleteAttribute.Message, + Does.Contain("removed in a future major version")); + Assert.That(editorBrowsableAttribute, Is.Not.Null); + Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never)); + }); + } + + /// + /// 断言类型级兼容扩展具备统一的弃用元数据。 + /// + /// 兼容扩展类型。 + /// 期望的迁移提示。 + private static void AssertLegacyType(Type type, string expectedReplacementHint) + { + var obsoleteAttribute = type.GetCustomAttribute(); + var editorBrowsableAttribute = type.GetCustomAttribute(); + + Assert.Multiple(() => + { + Assert.That(obsoleteAttribute, Is.Not.Null); + Assert.That(obsoleteAttribute!.Message, Does.Contain(expectedReplacementHint)); + Assert.That( + obsoleteAttribute.Message, + Does.Contain("removed in a future major version")); + Assert.That(editorBrowsableAttribute, Is.Not.Null); + Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never)); + }); + } +} diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index ec571ee8..0395347d 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; @@ -157,9 +158,12 @@ public abstract class Architecture : IArchitecture /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { RegisterCqrsPipelineBehavior(); diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index 7563e276..a0c1b149 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Logging; @@ -26,9 +27,12 @@ internal sealed class ArchitectureModules( /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { RegisterCqrsPipelineBehavior(); diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index 91ed0238..f955f175 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.ComponentModel; using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; @@ -21,8 +22,11 @@ namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead.")] +[EditorBrowsable(EditorBrowsableState.Never)] +[Obsolete( + "Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")] public static class MediatorCoroutineExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 9fc9311b..85f9776e 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs.Extensions; @@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.")] +[EditorBrowsable(EditorBrowsableState.Never)] +[Obsolete( + "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorCommandExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index 4b63e223..68b130ce 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs.Extensions; @@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 统一接口扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead.")] +[EditorBrowsable(EditorBrowsableState.Never)] +[Obsolete( + "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index be2d70e1..d7fada4a 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Cqrs.Extensions; @@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 /// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// -[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.")] +[EditorBrowsable(EditorBrowsableState.Never)] +[Obsolete( + "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorQueryExtensions { /// diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 69b46720..3b0396c1 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; @@ -360,9 +361,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// /// 注册 CQRS 请求管道行为。 /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 /// /// 行为类型,必须是引用类型 - [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { RegisterCqrsPipelineBehavior(); diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 3effaa86..a38fbdfa 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -223,7 +223,8 @@ public class GameArchitecture : Architecture 如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。 `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` -仅作为兼容名称保留。当前接口支持两种形式: +仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。 +`ContextAwareMediator*Extensions` 与 `MediatorCoroutineExtensions` 也遵循同样的弃用节奏。当前接口支持两种形式: - 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 - 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` From fd6442374175803cebff9cbe37ad6f4dc49cfa6e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:21:20 +0800 Subject: [PATCH 13/70] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E6=9E=B6=E6=9E=84=E6=A8=A1=E5=BC=8F=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 核心概念介绍,包括命令、查询、处理器和分发器 - 添加基本用法示例,展示命令和查询的定义与发送流程 - 实现高级功能文档,涵盖请求、通知、管道行为和流式处理 - 提供最佳实践指南,明确命令查询分离和验证行为使用方式 - 增加常见问题解答,解释 Command/Query 区别和错误处理方案 - 新增 CQRS 处理器自动注册实现,支持源码生成和反射扫描 - 添加单元测试验证处理器注册顺序和容错行为 - 更新项目 AI 代理说明文档,完善模块依赖关系图 --- CLAUDE.md | 1 + .../Cqrs/CqrsHandlerRegistryAttribute.cs | 18 + .../Cqrs/ICqrsHandlerRegistry.cs | 20 ++ .../Cqrs/CqrsHandlerRegistrarTests.cs | 119 +++++++ .../Cqrs/Internal/CqrsHandlerRegistrar.cs | 77 ++++- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 195 +++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 311 ++++++++++++++++++ docs/zh-CN/core/cqrs.md | 8 +- 8 files changed, 746 insertions(+), 3 deletions(-) create mode 100644 GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs create mode 100644 GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs create mode 100644 GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs create mode 100644 GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs diff --git a/CLAUDE.md b/CLAUDE.md index e2e656c3..4ea95597 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,7 @@ major 版本中移除。 - `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。 - `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。 - `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。 +- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。 这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。 diff --git a/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs b/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs new file mode 100644 index 00000000..e073fe5c --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs @@ -0,0 +1,18 @@ +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。 +/// +/// +/// 该特性通常由源码生成器自动添加到消费端程序集。 +/// 运行时读取到该特性后,会优先实例化对应的 , +/// 以常量时间获取处理器注册映射,而不是遍历程序集中的全部类型。 +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class CqrsHandlerRegistryAttribute(Type registryType) : Attribute +{ + /// + /// 获取承载 CQRS 处理器注册逻辑的注册器类型。 + /// + public Type RegistryType { get; } = registryType ?? throw new ArgumentNullException(nameof(registryType)); +} diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs new file mode 100644 index 00000000..91af6be7 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs @@ -0,0 +1,20 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义由源码生成器产出的 CQRS 处理器注册器契约。 +/// +/// +/// 运行时会优先调用实现该接口的程序集级注册器,以避免在冷启动阶段对整个程序集执行反射扫描。 +/// 当目标程序集没有生成注册器,或生成注册器因兼容性原因不可用时,运行时仍会回退到反射扫描路径。 +/// +public interface ICqrsHandlerRegistry +{ + /// + /// 将当前程序集中的 CQRS 处理器映射注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断信息的日志器。 + void Register(IServiceCollection services, ILogger logger); +} diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 28b1fb32..7748227c 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -115,6 +115,80 @@ internal sealed class CqrsHandlerRegistrarTests LoggerFactoryResolver.Provider = originalProvider; } } + + /// + /// 验证当程序集提供源码生成的注册器时,运行时会优先使用该注册器而不是反射扫描类型列表。 + /// + [Test] + public void RegisterHandlers_Should_Use_Generated_Registry_When_Available() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.GeneratedRegistryAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedNotificationHandlerRegistry))]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var handlers = container.GetAll>(); + + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + } + + /// + /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 + /// + [Test] + public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Invalid() + { + var originalProvider = LoggerFactoryResolver.Provider; + var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning); + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.InvalidGeneratedRegistryAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(string))]); + generatedAssembly + .Setup(static assembly => assembly.GetTypes()) + .Returns([typeof(AlphaDeterministicNotificationHandler)]); + + LoggerFactoryResolver.Provider = capturingProvider; + try + { + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var handlers = container.GetAll>(); + var warningLogs = capturingProvider.Loggers + .SelectMany(static logger => logger.Logs) + .Where(static log => log.Level == LogLevel.Warning) + .ToList(); + + Assert.Multiple(() => + { + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + Assert.That( + warningLogs.Any(log => + log.Message.Contains("does not implement", StringComparison.Ordinal)), + Is.True); + }); + } + finally + { + LoggerFactoryResolver.Provider = originalProvider; + } + } } /// @@ -219,3 +293,48 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider return logger; } } + +/// +/// 用于验证生成注册器路径的通知消息。 +/// +internal sealed record GeneratedRegistryNotification : INotification; + +/// +/// 由模拟的源码生成注册器显式注册的通知处理器。 +/// +internal sealed class GeneratedRegistryNotificationHandler : INotificationHandler +{ + /// + /// 处理生成注册器测试中的通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} + +/// +/// 模拟源码生成器为某个程序集生成的 CQRS 处理器注册器。 +/// +internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将测试通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs index 957c0981..189ae8f0 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -7,7 +7,8 @@ namespace GFramework.Core.Cqrs.Internal; /// /// 在架构初始化期间扫描并注册 CQRS 处理器。 -/// 首批实现采用运行时反射扫描,优先满足“无需额外注册步骤即可工作”的迁移目标。 +/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销; +/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。 /// internal static class CqrsHandlerRegistrar { @@ -31,10 +32,84 @@ internal static class CqrsHandlerRegistrar .Distinct() .OrderBy(GetAssemblySortKey, StringComparer.Ordinal)) { + if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger)) + continue; + RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger); } } + /// + /// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。 + /// + /// 目标服务集合。 + /// 当前要处理的程序集。 + /// 日志记录器。 + /// 当成功使用生成注册器时返回 ;否则返回 + private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + + try + { + var registryTypes = assembly + .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) + .OfType() + .Select(static attribute => attribute.RegistryType) + .Where(static type => type is not null) + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + + if (registryTypes.Count == 0) + return false; + + var registries = new List(registryTypes.Count); + foreach (var registryType in registryTypes) + { + if (!typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType)) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); + return false; + } + + if (registryType.IsAbstract) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); + return false; + } + + if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated."); + return false; + } + + registries.Add(registry); + } + + foreach (var registry in registries) + { + logger.Debug( + $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); + registry.Register(services, logger); + } + + return true; + } + catch (Exception exception) + { + logger.Warn( + $"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan."); + logger.Warn( + $"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}"); + return false; + } + } + /// /// 注册单个程序集里的所有 CQRS 处理器映射。 /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs new file mode 100644 index 00000000..b04d8165 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -0,0 +1,195 @@ +using GFramework.SourceGenerators.Cqrs; +using GFramework.SourceGenerators.Tests.Core; + +namespace GFramework.SourceGenerators.Tests.Cqrs; + +/// +/// 验证 CQRS 处理器注册生成器的输出与回退边界。 +/// +[TestFixture] +public class CqrsHandlerRegistryGeneratorTests +{ + /// + /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 + /// + [Test] + public async Task Generates_Assembly_Level_Cqrs_Handler_Registry() + { + const string source = """ + using System; + using System.Collections.Generic; + + 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.Core.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Core.Abstractions.Cqrs; + + public sealed record PingQuery() : IRequest; + public sealed record DomainEvent() : INotification; + public sealed record NumberStream() : IStreamRequest; + + public sealed class ZetaNotificationHandler : INotificationHandler { } + public sealed class AlphaQueryHandler : IRequestHandler { } + public sealed class StreamHandler : IStreamRequestHandler { } + } + """; + + const string expected = """ + // + #nullable enable + + [assembly: global::GFramework.Core.Abstractions.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Core.Abstractions.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Core.Abstractions.Cqrs.IRequestHandler), + typeof(global::TestApp.AlphaQueryHandler)); + logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Core.Abstractions.Cqrs.IRequestHandler."); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler), + typeof(global::TestApp.StreamHandler)); + logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler."); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Core.Abstractions.Cqrs.INotificationHandler), + typeof(global::TestApp.ZetaNotificationHandler)); + logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Core.Abstractions.Cqrs.INotificationHandler."); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", expected)); + } + + /// + /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。 + /// + [Test] + public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_Handler() + { + const string source = """ + using System; + + 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.Core.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Core.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + await test.RunAsync(); + } +} diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs new file mode 100644 index 00000000..59db7c77 --- /dev/null +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -0,0 +1,311 @@ +using GFramework.SourceGenerators.Common.Constants; + +namespace GFramework.SourceGenerators.Cqrs; + +/// +/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 +/// +[Generator] +public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator +{ + private const string CqrsNamespace = $"{PathContests.CoreAbstractionsNamespace}.Cqrs"; + private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging"; + private const string IRequestHandlerMetadataName = $"{CqrsNamespace}.IRequestHandler`2"; + private const string INotificationHandlerMetadataName = $"{CqrsNamespace}.INotificationHandler`1"; + private const string IStreamRequestHandlerMetadataName = $"{CqrsNamespace}.IStreamRequestHandler`2"; + private const string ICqrsHandlerRegistryMetadataName = $"{CqrsNamespace}.ICqrsHandlerRegistry"; + private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsNamespace}.CqrsHandlerRegistryAttribute"; + private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger"; + private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; + private const string GeneratedNamespace = "GFramework.Generated.Cqrs"; + private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry"; + private const string HintName = "CqrsHandlerRegistry.g.cs"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput( + context.CompilationProvider, + static (productionContext, compilation) => Execute(productionContext, compilation)); + } + + private static void Execute(SourceProductionContext context, Compilation compilation) + { + var requestHandlerType = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName); + var notificationHandlerType = compilation.GetTypeByMetadataName(INotificationHandlerMetadataName); + var streamHandlerType = compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName); + var registryInterfaceType = compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName); + var registryAttributeType = compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName); + var loggerType = compilation.GetTypeByMetadataName(ILoggerMetadataName); + var serviceCollectionType = compilation.GetTypeByMetadataName(IServiceCollectionMetadataName); + + if (requestHandlerType is null || + notificationHandlerType is null || + streamHandlerType is null || + registryInterfaceType is null || + registryAttributeType is null || + loggerType is null || + serviceCollectionType is null) + { + return; + } + + var registrations = CollectRegistrations( + compilation.Assembly.GlobalNamespace, + requestHandlerType, + notificationHandlerType, + streamHandlerType, + out var hasUnsupportedConcreteHandler); + + // If the assembly contains handlers that generated code cannot legally reference + // (for example private nested handlers), keep the runtime on the reflection path + // so registration behavior remains complete instead of silently dropping handlers. + if (hasUnsupportedConcreteHandler || registrations.Count == 0) + return; + + context.AddSource(HintName, GenerateSource(registrations)); + } + + private static List CollectRegistrations( + INamespaceSymbol rootNamespace, + INamedTypeSymbol requestHandlerType, + INamedTypeSymbol notificationHandlerType, + INamedTypeSymbol streamHandlerType, + out bool hasUnsupportedConcreteHandler) + { + var registrations = new List(); + hasUnsupportedConcreteHandler = false; + + foreach (var type in EnumerateTypes(rootNamespace)) + { + if (!IsConcreteHandlerType(type)) + continue; + + var handlerInterfaces = type.AllInterfaces + .Where(interfaceType => IsSupportedHandlerInterface( + interfaceType, + requestHandlerType, + notificationHandlerType, + streamHandlerType)) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + + if (handlerInterfaces.Count == 0) + continue; + + if (!CanReferenceFromGeneratedRegistry(type) || + handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) + { + hasUnsupportedConcreteHandler = true; + return []; + } + + var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var implementationLogName = GetLogDisplayName(type); + + foreach (var handlerInterface in handlerInterfaces) + { + registrations.Add(new HandlerRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + implementationTypeDisplayName, + GetLogDisplayName(handlerInterface), + implementationLogName)); + } + } + + registrations.Sort(static (left, right) => + { + var implementationComparison = StringComparer.Ordinal.Compare( + left.ImplementationLogName, + right.ImplementationLogName); + + return implementationComparison != 0 + ? implementationComparison + : StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName); + }); + + return registrations; + } + + private static IEnumerable EnumerateTypes(INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) + { + switch (member) + { + case INamespaceSymbol childNamespace: + foreach (var type in EnumerateTypes(childNamespace)) + yield return type; + + break; + + case INamedTypeSymbol namedType: + foreach (var type in EnumerateTypes(namedType)) + yield return type; + + break; + } + } + } + + private static IEnumerable EnumerateTypes(INamedTypeSymbol typeSymbol) + { + yield return typeSymbol; + + foreach (var nestedType in typeSymbol.GetTypeMembers()) + { + foreach (var descendant in EnumerateTypes(nestedType)) + yield return descendant; + } + } + + private static bool IsConcreteHandlerType(INamedTypeSymbol type) + { + return type.TypeKind is TypeKind.Class or TypeKind.Struct && + !type.IsAbstract && + !ContainsGenericParameters(type); + } + + private static bool ContainsGenericParameters(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.ContainingType) + { + if (current.TypeParameters.Length > 0) + return true; + } + + return false; + } + + private static bool IsSupportedHandlerInterface( + INamedTypeSymbol interfaceType, + INamedTypeSymbol requestHandlerType, + INamedTypeSymbol notificationHandlerType, + INamedTypeSymbol streamHandlerType) + { + if (!interfaceType.IsGenericType) + return false; + + var definition = interfaceType.OriginalDefinition; + return SymbolEqualityComparer.Default.Equals(definition, requestHandlerType) || + SymbolEqualityComparer.Default.Equals(definition, notificationHandlerType) || + SymbolEqualityComparer.Default.Equals(definition, streamHandlerType); + } + + private static bool CanReferenceFromGeneratedRegistry(ITypeSymbol type) + { + switch (type) + { + case IArrayTypeSymbol arrayType: + return CanReferenceFromGeneratedRegistry(arrayType.ElementType); + case INamedTypeSymbol namedType: + if (!IsTypeChainAccessible(namedType)) + return false; + + return namedType.TypeArguments.All(CanReferenceFromGeneratedRegistry); + case IPointerTypeSymbol pointerType: + return CanReferenceFromGeneratedRegistry(pointerType.PointedAtType); + case ITypeParameterSymbol: + return false; + default: + return true; + } + } + + private static bool IsTypeChainAccessible(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.ContainingType) + { + if (!IsSymbolAccessible(current)) + return false; + } + + return true; + } + + private static bool IsSymbolAccessible(ISymbol symbol) + { + return symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal + or Accessibility.ProtectedOrInternal; + } + + private static string GetTypeSortKey(ITypeSymbol type) + { + return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + private static string GetLogDisplayName(ITypeSymbol type) + { + return GetTypeSortKey(type).Replace("global::", string.Empty); + } + + private static string GenerateSource(IReadOnlyList registrations) + { + var builder = new StringBuilder(); + builder.AppendLine("// "); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + builder.Append("[assembly: global::"); + builder.Append(CqrsNamespace); + builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); + builder.Append(GeneratedNamespace); + builder.Append('.'); + builder.Append(GeneratedTypeName); + builder.AppendLine("))]"); + builder.AppendLine(); + builder.Append("namespace "); + builder.Append(GeneratedNamespace); + builder.AppendLine(";"); + builder.AppendLine(); + builder.Append("internal sealed class "); + builder.Append(GeneratedTypeName); + builder.Append(" : global::"); + builder.Append(CqrsNamespace); + builder.AppendLine(".ICqrsHandlerRegistry"); + builder.AppendLine("{"); + builder.Append( + " public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::"); + builder.Append(LoggingNamespace); + builder.AppendLine(".ILogger logger)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (services is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); + builder.AppendLine(" if (logger is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); + builder.AppendLine(); + + foreach (var registration in registrations) + { + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(registration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" typeof("); + builder.Append(registration.ImplementationTypeDisplayName); + builder.AppendLine("));"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(registration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + private static string EscapeStringLiteral(string value) + { + return value.Replace("\\", "\\\\") + .Replace("\"", "\\\""); + } + + private readonly record struct HandlerRegistrationSpec( + string HandlerInterfaceDisplayName, + string ImplementationTypeDisplayName, + string HandlerInterfaceLogName, + string ImplementationLogName); +} diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index a38fbdfa..5c0a2203 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -204,7 +204,7 @@ public async Task> GetHighScores() ### 注册处理器 -在架构中注册 CQRS 行为;默认会自动扫描当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: +在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: ```csharp public class GameArchitecture : Architecture @@ -220,7 +220,11 @@ public class GameArchitecture : Architecture } ``` -如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。 +当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器; +如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。 +`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。 + +如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围。 `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` 仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。 From 7a6f966601d235abbd20fb2b8470409b6636ddec Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:12:36 +0800 Subject: [PATCH 14/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsHandlerRegistryGenerator 源代码生成器 - 支持 IRequestHandler、INotificationHandler 和 IStreamRequestHandler 接口的处理器注册 - 生成程序集级别的 CQRS 处理器注册器以减少运行时反射开销 - 添加对请求、通知和流处理器的稳定顺序注册支持 - 实现对私有嵌套处理器的检测和回退机制 - 提供字符串字面量转义功能以避免生成代码中的语法错误 - 添加完整的单元测试验证生成器的功能和边界条件 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 20 ++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 279 ++++++++++++------ 2 files changed, 203 insertions(+), 96 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index b04d8165..e2b7ffb1 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using GFramework.SourceGenerators.Cqrs; using GFramework.SourceGenerators.Tests.Core; @@ -192,4 +193,23 @@ public class CqrsHandlerRegistryGeneratorTests await test.RunAsync(); } + + /// + /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 + /// + [Test] + public void Escape_String_Literal_Handles_Control_Characters() + { + var method = typeof(CqrsHandlerRegistryGenerator).GetMethod( + "EscapeStringLiteral", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(method, Is.Not.Null); + + const string input = "line1\r\nline2\\\""; + const string expected = "line1\\r\\nline2\\\\\\\""; + var escaped = method!.Invoke(null, [input]) as string; + + Assert.That(escaped, Is.EqualTo(expected)); + } } diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 59db7c77..39c3aadb 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -24,38 +24,93 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator /// public void Initialize(IncrementalGeneratorInitializationContext context) { + var generationEnabled = context.CompilationProvider + .Select(static (compilation, _) => HasRequiredTypes(compilation)); + + // Restrict semantic analysis to type declarations that can actually contribute implemented interfaces. + var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => IsHandlerCandidate(node), + static (syntaxContext, _) => TransformHandlerCandidate(syntaxContext)) + .Where(static candidate => candidate is not null) + .Collect(); + context.RegisterSourceOutput( - context.CompilationProvider, - static (productionContext, compilation) => Execute(productionContext, compilation)); + generationEnabled.Combine(handlerCandidates), + static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right)); } - private static void Execute(SourceProductionContext context, Compilation compilation) + private static bool HasRequiredTypes(Compilation compilation) { - var requestHandlerType = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName); - var notificationHandlerType = compilation.GetTypeByMetadataName(INotificationHandlerMetadataName); - var streamHandlerType = compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName); - var registryInterfaceType = compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName); - var registryAttributeType = compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName); - var loggerType = compilation.GetTypeByMetadataName(ILoggerMetadataName); - var serviceCollectionType = compilation.GetTypeByMetadataName(IServiceCollectionMetadataName); + return compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null && + compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName) is not null && + compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && + compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; + } - if (requestHandlerType is null || - notificationHandlerType is null || - streamHandlerType is null || - registryInterfaceType is null || - registryAttributeType is null || - loggerType is null || - serviceCollectionType is null) + private static bool IsHandlerCandidate(SyntaxNode node) + { + return node is TypeDeclarationSyntax { - return; + BaseList.Types.Count: > 0 + }; + } + + private static HandlerCandidateAnalysis? TransformHandlerCandidate(GeneratorSyntaxContext context) + { + if (context.Node is not TypeDeclarationSyntax typeDeclaration) + return null; + + if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol type) + return null; + + if (!IsConcreteHandlerType(type)) + return null; + + var handlerInterfaces = type.AllInterfaces + .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToImmutableArray(); + + if (handlerInterfaces.IsDefaultOrEmpty) + return null; + + var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (!CanReferenceFromGeneratedRegistry(type) || + handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) + { + return new HandlerCandidateAnalysis( + implementationTypeDisplayName, + ImmutableArray.Empty, + true); } - var registrations = CollectRegistrations( - compilation.Assembly.GlobalNamespace, - requestHandlerType, - notificationHandlerType, - streamHandlerType, - out var hasUnsupportedConcreteHandler); + var implementationLogName = GetLogDisplayName(type); + var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); + foreach (var handlerInterface in handlerInterfaces) + { + registrations.Add(new HandlerRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + implementationTypeDisplayName, + GetLogDisplayName(handlerInterface), + implementationLogName)); + } + + return new HandlerCandidateAnalysis( + implementationTypeDisplayName, + registrations.MoveToImmutable(), + false); + } + + private static void Execute(SourceProductionContext context, bool generationEnabled, + ImmutableArray candidates) + { + if (!generationEnabled) + return; + + var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler); // If the assembly contains handlers that generated code cannot legally reference // (for example private nested handlers), keep the runtime on the reflection path @@ -67,50 +122,33 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } private static List CollectRegistrations( - INamespaceSymbol rootNamespace, - INamedTypeSymbol requestHandlerType, - INamedTypeSymbol notificationHandlerType, - INamedTypeSymbol streamHandlerType, + ImmutableArray candidates, out bool hasUnsupportedConcreteHandler) { var registrations = new List(); hasUnsupportedConcreteHandler = false; - foreach (var type in EnumerateTypes(rootNamespace)) + // Partial declarations surface the same symbol through multiple syntax nodes. + // Collapse them by implementation type so generated registrations stay stable and duplicate-free. + var uniqueCandidates = new Dictionary(StringComparer.Ordinal); + + foreach (var candidate in candidates) { - if (!IsConcreteHandlerType(type)) + if (candidate is null) continue; - var handlerInterfaces = type.AllInterfaces - .Where(interfaceType => IsSupportedHandlerInterface( - interfaceType, - requestHandlerType, - notificationHandlerType, - streamHandlerType)) - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); - - if (handlerInterfaces.Count == 0) - continue; - - if (!CanReferenceFromGeneratedRegistry(type) || - handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) + if (candidate.Value.HasUnsupportedConcreteHandler) { hasUnsupportedConcreteHandler = true; return []; } - var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var implementationLogName = GetLogDisplayName(type); + uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value; + } - foreach (var handlerInterface in handlerInterfaces) - { - registrations.Add(new HandlerRegistrationSpec( - handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - implementationTypeDisplayName, - GetLogDisplayName(handlerInterface), - implementationLogName)); - } + foreach (var candidate in uniqueCandidates.Values) + { + registrations.AddRange(candidate.Registrations); } registrations.Sort(static (left, right) => @@ -127,38 +165,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return registrations; } - private static IEnumerable EnumerateTypes(INamespaceSymbol namespaceSymbol) - { - foreach (var member in namespaceSymbol.GetMembers()) - { - switch (member) - { - case INamespaceSymbol childNamespace: - foreach (var type in EnumerateTypes(childNamespace)) - yield return type; - - break; - - case INamedTypeSymbol namedType: - foreach (var type in EnumerateTypes(namedType)) - yield return type; - - break; - } - } - } - - private static IEnumerable EnumerateTypes(INamedTypeSymbol typeSymbol) - { - yield return typeSymbol; - - foreach (var nestedType in typeSymbol.GetTypeMembers()) - { - foreach (var descendant in EnumerateTypes(nestedType)) - yield return descendant; - } - } - private static bool IsConcreteHandlerType(INamedTypeSymbol type) { return type.TypeKind is TypeKind.Class or TypeKind.Struct && @@ -177,19 +183,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } - private static bool IsSupportedHandlerInterface( - INamedTypeSymbol interfaceType, - INamedTypeSymbol requestHandlerType, - INamedTypeSymbol notificationHandlerType, - INamedTypeSymbol streamHandlerType) + private static bool IsSupportedHandlerInterface(INamedTypeSymbol interfaceType) { if (!interfaceType.IsGenericType) return false; - var definition = interfaceType.OriginalDefinition; - return SymbolEqualityComparer.Default.Equals(definition, requestHandlerType) || - SymbolEqualityComparer.Default.Equals(definition, notificationHandlerType) || - SymbolEqualityComparer.Default.Equals(definition, streamHandlerType); + var definitionMetadataName = GetFullyQualifiedMetadataName(interfaceType.OriginalDefinition); + return string.Equals(definitionMetadataName, IRequestHandlerMetadataName, StringComparison.Ordinal) || + string.Equals(definitionMetadataName, INotificationHandlerMetadataName, StringComparison.Ordinal) || + string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal); } private static bool CanReferenceFromGeneratedRegistry(ITypeSymbol type) @@ -229,6 +231,31 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator or Accessibility.ProtectedOrInternal; } + private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type) + { + var nestedTypes = new Stack(); + for (var current = type; current is not null; current = current.ContainingType) + { + nestedTypes.Push(current.MetadataName); + } + + var builder = new StringBuilder(); + if (!type.ContainingNamespace.IsGlobalNamespace) + { + builder.Append(type.ContainingNamespace.ToDisplayString()); + builder.Append('.'); + } + + while (nestedTypes.Count > 0) + { + builder.Append(nestedTypes.Pop()); + if (nestedTypes.Count > 0) + builder.Append('.'); + } + + return builder.ToString(); + } + private static string GetTypeSortKey(ITypeSymbol type) { return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); @@ -300,7 +327,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private static string EscapeStringLiteral(string value) { return value.Replace("\\", "\\\\") - .Replace("\"", "\\\""); + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r"); } private readonly record struct HandlerRegistrationSpec( @@ -308,4 +337,62 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string ImplementationTypeDisplayName, string HandlerInterfaceLogName, string ImplementationLogName); + + private readonly struct HandlerCandidateAnalysis : IEquatable + { + public HandlerCandidateAnalysis( + string implementationTypeDisplayName, + ImmutableArray registrations, + bool hasUnsupportedConcreteHandler) + { + ImplementationTypeDisplayName = implementationTypeDisplayName; + Registrations = registrations; + HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler; + } + + public string ImplementationTypeDisplayName { get; } + + public ImmutableArray Registrations { get; } + + public bool HasUnsupportedConcreteHandler { get; } + + public bool Equals(HandlerCandidateAnalysis other) + { + if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, + StringComparison.Ordinal) || + HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler || + Registrations.Length != other.Registrations.Length) + { + return false; + } + + for (var index = 0; index < Registrations.Length; index++) + { + if (!Registrations[index].Equals(other.Registrations[index])) + return false; + } + + return true; + } + + public override bool Equals(object? obj) + { + return obj is HandlerCandidateAnalysis other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName); + hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode(); + foreach (var registration in Registrations) + { + hashCode = (hashCode * 397) ^ registration.GetHashCode(); + } + + return hashCode; + } + } + } } From 4db79235129c672db43f00a1957cc02c67b9782d Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:44:44 +0800 Subject: [PATCH 15/70] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E6=9E=B6=E6=9E=84=E6=A8=A1=E5=BC=8F=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完整介绍 CQRS 核心概念包括命令、查询、处理器和分发器 - 提供命令和查询的定义与实现示例代码 - 详细介绍处理器编写方法和注册流程 - 说明管道行为(Behaviors)的使用方式 - 展示通知(Notification)和流式处理功能 - 提供最佳实践和常见问题解决方案 - 包含完整的 API 参考和用法示例 --- CLAUDE.md | 3 +- .../Architectures/IArchitecture.cs | 16 +- GFramework.Core.Abstractions/GlobalUsings.cs | 3 +- .../Ioc/IIocContainer.cs | 17 +- ...ArchitectureAdditionalCqrsHandlersTests.cs | 165 ++++++++++++++++++ .../RegistryInitializationHookBaseTests.cs | 21 +++ GFramework.Core/Architectures/Architecture.cs | 21 +++ .../Architectures/ArchitectureBootstrapper.cs | 10 +- .../Architectures/ArchitectureModules.cs | 23 +++ GFramework.Core/Ioc/MicrosoftDiContainer.cs | 69 ++++++++ docs/zh-CN/core/cqrs.md | 22 ++- 11 files changed, 360 insertions(+), 10 deletions(-) create mode 100644 GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 4ea95597..da7e13c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,7 +105,8 @@ major 版本中移除。 - `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。 - `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。 - `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。 -- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。 +- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描;非默认程序集可通过 + `RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 显式接入同一路径。 这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。 diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index 16386d5a..ab54bd54 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -1,9 +1,9 @@ using System.ComponentModel; +using System.Reflection; using GFramework.Core.Abstractions.Lifecycle; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Architectures; @@ -96,6 +96,20 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia void RegisterMediatorBehavior() where TBehavior : class; + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + void RegisterCqrsHandlersFromAssembly(Assembly assembly); + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。 + /// + /// 要接入的程序集集合。 + void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); + /// /// 安装架构模块 /// diff --git a/GFramework.Core.Abstractions/GlobalUsings.cs b/GFramework.Core.Abstractions/GlobalUsings.cs index 7c0f226a..204433de 100644 --- a/GFramework.Core.Abstractions/GlobalUsings.cs +++ b/GFramework.Core.Abstractions/GlobalUsings.cs @@ -16,4 +16,5 @@ global using System.Collections.Generic; global using System.Runtime; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index aead5a9d..b1908020 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -1,7 +1,7 @@ using System.ComponentModel; +using System.Reflection; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Abstractions.Ioc; @@ -109,6 +109,21 @@ public interface IIocContainer : IContextAware void RegisterMediatorBehavior() where TBehavior : class; + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。 + /// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + void RegisterCqrsHandlersFromAssembly(Assembly assembly); + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。 + /// + /// 要接入的程序集集合。 + void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); + /// /// 配置服务 diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs new file mode 100644 index 00000000..ae0bbad0 --- /dev/null +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -0,0 +1,165 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Architectures; +using GFramework.Core.Logging; + +namespace GFramework.Core.Tests.Architectures; + +/// +/// 验证架构初始化阶段可以显式接入默认程序集之外的 CQRS handlers。 +/// +[TestFixture] +public sealed class ArchitectureAdditionalCqrsHandlersTests +{ + /// + /// 初始化日志工厂和共享测试状态。 + /// + [SetUp] + public void SetUp() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + GameContext.Clear(); + AdditionalAssemblyNotificationHandler.Reset(); + } + + /// + /// 清理测试过程中写入的共享状态。 + /// + [TearDown] + public void TearDown() + { + AdditionalAssemblyNotificationHandler.Reset(); + GameContext.Clear(); + } + + /// + /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 + /// + [Test] + public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly() + { + var generatedAssembly = CreateGeneratedHandlerAssembly(); + var architecture = new AdditionalHandlersTestArchitecture(target => + target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object)); + + await architecture.InitializeAsync(); + await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); + + Assert.That(AdditionalAssemblyNotificationHandler.InvocationCount, Is.EqualTo(1)); + + await architecture.DestroyAsync(); + } + + /// + /// 验证同一额外程序集被重复声明时,不会向容器重复写入相同 handler 映射。 + /// + [Test] + public async Task RegisterCqrsHandlersFromAssembly_Should_Deduplicate_Repeated_Assembly_Registration() + { + var generatedAssembly = CreateGeneratedHandlerAssembly(); + var architecture = new AdditionalHandlersTestArchitecture(target => + { + target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object); + target.RegisterCqrsHandlersFromAssemblies([generatedAssembly.Object]); + }); + + await architecture.InitializeAsync(); + await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); + + Assert.That(AdditionalAssemblyNotificationHandler.InvocationCount, Is.EqualTo(1)); + + await architecture.DestroyAsync(); + } + + /// + /// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。 + /// 该测试替身模拟“扩展程序集已经挂接 source-generator,运行时只需显式接入该程序集”的真实路径。 + /// + /// 包含程序集级 handler registry 元数据的 mocked Assembly。 + private static Mock CreateGeneratedHandlerAssembly() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]); + return generatedAssembly; + } + + /// + /// 用于测试额外程序集注册入口的最小架构实现。 + /// + private sealed class AdditionalHandlersTestArchitecture(Action configure) : + Architecture + { + /// + /// 在初始化阶段执行测试注入的额外 CQRS 程序集接入逻辑。 + /// + protected override void OnInitialize() + { + configure(this); + } + } +} + +/// +/// 用于验证额外程序集接入是否成功的测试通知。 +/// +public sealed record AdditionalAssemblyNotification : INotification; + +/// +/// 由模拟扩展程序集的生成注册器挂入当前容器的通知处理器。 +/// +public sealed class AdditionalAssemblyNotificationHandler : INotificationHandler +{ + /// + /// 获取当前测试进程中该处理器的执行次数。 + /// + public static int InvocationCount { get; private set; } + + /// + /// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(AdditionalAssemblyNotification notification, CancellationToken cancellationToken) + { + InvocationCount++; + return ValueTask.CompletedTask; + } + + /// + /// 清理共享计数器,避免测试间相互污染。 + /// + public static void Reset() + { + InvocationCount = 0; + } +} + +/// +/// 模拟由 source-generator 为扩展程序集生成的 CQRS handler registry。 +/// +internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将扩展程序集中的通知处理器映射写入服务集合。 + /// + /// 目标服务集合。 + /// 日志记录器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services + .AddTransient, + AdditionalAssemblyNotificationHandler>(); + logger.Debug( + $"Registered CQRS handler {typeof(AdditionalAssemblyNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs index 38e06805..66f7869c 100644 --- a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs +++ b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Lifecycle; @@ -185,6 +186,16 @@ public class TestArchitectureWithRegistry : IArchitecture throw new NotImplementedException(); } + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + throw new NotImplementedException(); + } + + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + throw new NotImplementedException(); + } + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] public void RegisterMediatorBehavior() where TBehavior : class { @@ -316,6 +327,16 @@ public class TestArchitectureWithoutRegistry : IArchitecture throw new NotImplementedException(); } + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + throw new NotImplementedException(); + } + + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + throw new NotImplementedException(); + } + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] public void RegisterMediatorBehavior() where TBehavior : class { diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 0395347d..0fb361a4 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; @@ -169,6 +170,26 @@ public abstract class Architecture : IArchitecture RegisterCqrsPipelineBehavior(); } + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + _modules.RegisterCqrsHandlersFromAssembly(assembly); + } + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。 + /// + /// 要接入的程序集集合。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + _modules.RegisterCqrsHandlersFromAssemblies(assemblies); + } + /// /// 安装架构模块 /// diff --git a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs index d022c806..343bcdd6 100644 --- a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs +++ b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs @@ -1,7 +1,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Cqrs.Internal; namespace GFramework.Core.Architectures; @@ -99,10 +98,11 @@ internal sealed class ArchitectureBootstrapper( private void ConfigureServices(IArchitectureContext context, Action? configurator) { services.SetContext(context); - CqrsHandlerRegistrar.RegisterHandlers( - services.Container, - [architectureType.Assembly, typeof(ArchitectureContext).Assembly], - logger); + services.Container.RegisterCqrsHandlersFromAssemblies( + [ + architectureType.Assembly, + typeof(ArchitectureContext).Assembly + ]); if (configurator is null) logger.Debug("No external service configurator provided. Using built-in CQRS runtime registration only."); diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index a0c1b149..8e659fa4 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Logging; @@ -38,6 +39,28 @@ internal sealed class ArchitectureModules( RegisterCqrsPipelineBehavior(); } + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}"); + services.Container.RegisterCqrsHandlersFromAssembly(assembly); + } + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。 + /// + /// 要接入的程序集集合。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + logger.Debug("Registering CQRS handlers from additional assemblies."); + services.Container.RegisterCqrsHandlersFromAssemblies(assemblies); + } + /// /// 安装架构模块 /// diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 3b0396c1..b3dfcb03 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,9 +1,11 @@ using System.ComponentModel; +using System.Reflection; using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; +using GFramework.Core.Cqrs.Internal; using GFramework.Core.Logging; using GFramework.Core.Rule; @@ -56,6 +58,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// private readonly HashSet _registeredInstances = []; + /// + /// 已接入 CQRS handler 注册流程的程序集键集合。 + /// 使用稳定字符串键而不是 Assembly 引用本身,以避免默认路径和显式扩展路径使用不同 Assembly 对象时重复注册。 + /// + private readonly HashSet _registeredCqrsHandlerAssemblyKeys = new(StringComparer.Ordinal); + /// /// 日志记录器,用于记录容器操作日志 /// @@ -372,6 +380,56 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) RegisterCqrsPipelineBehavior(); } + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + ArgumentNullException.ThrowIfNull(assembly); + RegisterCqrsHandlersFromAssemblies([assembly]); + } + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。 + /// + /// 要接入的程序集集合。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + + _lock.EnterWriteLock(); + try + { + ThrowIfFrozen(); + + var processedAssemblyKeys = new HashSet(StringComparer.Ordinal); + foreach (var assembly in assemblies + .Where(static assembly => assembly is not null) + .OrderBy(GetCqrsAssemblyRegistrationKey, StringComparer.Ordinal)) + { + var assemblyKey = GetCqrsAssemblyRegistrationKey(assembly); + if (!processedAssemblyKeys.Add(assemblyKey)) + continue; + + if (_registeredCqrsHandlerAssemblyKeys.Contains(assemblyKey)) + { + _logger.Debug( + $"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered."); + continue; + } + + CqrsHandlerRegistrar.RegisterHandlers(this, [assembly], _logger); + _registeredCqrsHandlerAssemblyKeys.Add(assemblyKey); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + /// /// 配置服务 /// @@ -816,5 +874,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 生成 CQRS handler 注册用的稳定程序集键。 + /// 该键需要同时兼顾真实程序集与测试中使用的 mocked Assembly,避免仅靠引用比较导致重复接入。 + /// + /// 目标程序集。 + /// 稳定的程序集标识字符串。 + private static string GetCqrsAssemblyRegistrationKey(Assembly assembly) + { + return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString(); + } + #endregion } diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 5c0a2203..9d2410fe 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -224,7 +224,27 @@ public class GameArchitecture : Architecture 如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。 `GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。 -如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围。 +如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围: + +```csharp +public class GameArchitecture : Architecture +{ + protected override void OnInitialize() + { + RegisterCqrsPipelineBehavior>(); + + RegisterCqrsHandlersFromAssemblies( + [ + typeof(InventoryCqrsMarker).Assembly, + typeof(BattleCqrsMarker).Assembly + ]); + } +} +``` + +`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑: +优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册 +handler。 `RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` 仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。 From 27266d037d7052afe5cdfdb29d13b2f9ccc249a4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:38:45 +0800 Subject: [PATCH 16/70] =?UTF-8?q?feat(arch):=20=E6=B7=BB=E5=8A=A0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=9F=BA=E7=A1=80=E7=B1=BB=E5=92=8C=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E5=AE=B9=E5=99=A8=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 Architecture 基类提供系统、模型、工具等组件的注册与管理功能 - 实现架构生命周期管理、初始化流程控制和阶段转换功能 - 添加 ArchitectureModules 模块管理器负责 CQRS 行为注册和模块安装 - 实现 MicrosoftDiContainer 依赖注入容器适配器 - 支持单例、瞬态、作用域服务注册和工厂方法注册 - 添加 CQRS 请求管道行为和处理器注册功能 - 实现线程安全的读写锁保护容器操作 - 提供服务获取、排序和优先级管理功能 --- .../Architectures/IArchitecture.cs | 4 + .../Ioc/IIocContainer.cs | 4 + ...ArchitectureAdditionalCqrsHandlersTests.cs | 80 +++++++++++++------ .../RegistryInitializationHookBaseTests.cs | 20 +++++ .../Ioc/MicrosoftDiContainerTests.cs | 32 +++++++- GFramework.Core/Architectures/Architecture.cs | 4 + .../Architectures/ArchitectureModules.cs | 6 ++ GFramework.Core/Ioc/MicrosoftDiContainer.cs | 5 ++ 8 files changed, 130 insertions(+), 25 deletions(-) diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index ab54bd54..e1687ff9 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -101,6 +101,8 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 void RegisterCqrsHandlersFromAssembly(Assembly assembly); /// @@ -108,6 +110,8 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。 /// /// 要接入的程序集集合。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); /// diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index b1908020..3149b3c4 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -115,6 +115,8 @@ public interface IIocContainer : IContextAware /// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 void RegisterCqrsHandlersFromAssembly(Assembly assembly); /// @@ -122,6 +124,8 @@ public interface IIocContainer : IContextAware /// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。 /// /// 要接入的程序集集合。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index ae0bbad0..23725e7c 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -18,9 +18,10 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests [SetUp] public void SetUp() { + _previousLoggerFactoryProvider = LoggerFactoryResolver.Provider; LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); GameContext.Clear(); - AdditionalAssemblyNotificationHandler.Reset(); + AdditionalAssemblyNotificationHandlerState.Reset(); } /// @@ -29,10 +30,15 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests [TearDown] public void TearDown() { - AdditionalAssemblyNotificationHandler.Reset(); + AdditionalAssemblyNotificationHandlerState.Reset(); GameContext.Clear(); + LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider + ?? throw new InvalidOperationException( + "LoggerFactoryResolver.Provider should be captured during setup."); } + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + /// /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 /// @@ -44,11 +50,16 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object)); await architecture.InitializeAsync(); - await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); + try + { + await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); - Assert.That(AdditionalAssemblyNotificationHandler.InvocationCount, Is.EqualTo(1)); - - await architecture.DestroyAsync(); + Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1)); + } + finally + { + await architecture.DestroyAsync(); + } } /// @@ -65,11 +76,16 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests }); await architecture.InitializeAsync(); - await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); + try + { + await architecture.Context.PublishAsync(new AdditionalAssemblyNotification()); - Assert.That(AdditionalAssemblyNotificationHandler.InvocationCount, Is.EqualTo(1)); - - await architecture.DestroyAsync(); + Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1)); + } + finally + { + await architecture.DestroyAsync(); + } } /// @@ -111,25 +127,26 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests public sealed record AdditionalAssemblyNotification : INotification; /// -/// 由模拟扩展程序集的生成注册器挂入当前容器的通知处理器。 +/// 记录模拟扩展程序集通知处理器的执行次数。 /// -public sealed class AdditionalAssemblyNotificationHandler : INotificationHandler +public static class AdditionalAssemblyNotificationHandlerState { + private static int _invocationCount; + /// /// 获取当前测试进程中该处理器的执行次数。 /// - public static int InvocationCount { get; private set; } + /// + /// 该计数器通过原子读写维护,以支持 NUnit 并行执行环境中的并发访问。 + /// + public static int InvocationCount => Volatile.Read(ref _invocationCount); /// /// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。 /// - /// 通知实例。 - /// 取消令牌。 - /// 已完成任务。 - public ValueTask Handle(AdditionalAssemblyNotification notification, CancellationToken cancellationToken) + public static void RecordInvocation() { - InvocationCount++; - return ValueTask.CompletedTask; + Interlocked.Increment(ref _invocationCount); } /// @@ -137,7 +154,7 @@ public sealed class AdditionalAssemblyNotificationHandler : INotificationHandler /// public static void Reset() { - InvocationCount = 0; + Interlocked.Exchange(ref _invocationCount, 0); } } @@ -156,10 +173,25 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(logger); - services - .AddTransient, - AdditionalAssemblyNotificationHandler>(); + services.AddTransient>(_ => CreateHandler()); logger.Debug( - $"Registered CQRS handler {typeof(AdditionalAssemblyNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + $"Registered CQRS handler proxy for {typeof(INotificationHandler).FullName}."); + } + + /// + /// 创建一个仅供显式程序集注册路径使用的动态通知处理器。 + /// + /// 用于记录通知触发次数的测试替身处理器。 + private static INotificationHandler CreateHandler() + { + var handler = new Mock>(); + handler + .Setup(target => target.Handle(It.IsAny(), It.IsAny())) + .Returns(() => + { + AdditionalAssemblyNotificationHandlerState.RecordInvocation(); + return ValueTask.CompletedTask; + }); + return handler.Object; } } diff --git a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs index 66f7869c..b224804d 100644 --- a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs +++ b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs @@ -186,11 +186,21 @@ public class TestArchitectureWithRegistry : IArchitecture throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 要接入的程序集集合。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { throw new NotImplementedException(); @@ -327,11 +337,21 @@ public class TestArchitectureWithoutRegistry : IArchitecture throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 要接入的程序集集合。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { throw new NotImplementedException(); diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 0621bcfc..b4e5af67 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,7 +1,9 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Ioc; using GFramework.Core.Logging; +using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; namespace GFramework.Core.Tests.Ioc; @@ -306,6 +308,34 @@ public class MicrosoftDiContainerTests Assert.That(_container.Contains(), Is.False); } + /// + /// 测试清空容器后可以重新接入同一程序集中的 CQRS 处理器。 + /// + [Test] + public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State() + { + var assembly = typeof(CqrsHandlerRegistrarTests).Assembly; + + _container.RegisterCqrsHandlersFromAssembly(assembly); + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); + + _container.Clear(); + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.False); + + _container.RegisterCqrsHandlersFromAssembly(assembly); + + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); + } + /// /// 测试冻结容器以防止进一步注册的功能 /// @@ -676,4 +706,4 @@ public sealed class PrioritizedService : IPrioritizedService, IMixedService public sealed class NonPrioritizedService : IMixedService { public string? Name { get; set; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 0fb361a4..8d2582aa 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -175,6 +175,8 @@ public abstract class Architecture : IArchitecture /// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { _modules.RegisterCqrsHandlersFromAssembly(assembly); @@ -185,6 +187,8 @@ public abstract class Architecture : IArchitecture /// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。 /// /// 要接入的程序集集合。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { _modules.RegisterCqrsHandlersFromAssemblies(assemblies); diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index 8e659fa4..f5d2a55d 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -44,8 +44,11 @@ internal sealed class ArchitectureModules( /// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { + ArgumentNullException.ThrowIfNull(assembly); logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}"); services.Container.RegisterCqrsHandlersFromAssembly(assembly); } @@ -55,8 +58,11 @@ internal sealed class ArchitectureModules( /// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。 /// /// 要接入的程序集集合。 + /// + /// 底层容器已冻结,无法继续注册处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { + ArgumentNullException.ThrowIfNull(assemblies); logger.Debug("Registering CQRS handlers from additional assemblies."); services.Container.RegisterCqrsHandlersFromAssemblies(assemblies); } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index b3dfcb03..712a41c1 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -384,6 +384,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 从指定程序集显式注册 CQRS 处理器。 /// /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 public void RegisterCqrsHandlersFromAssembly(Assembly assembly) { ArgumentNullException.ThrowIfNull(assembly); @@ -395,6 +397,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。 /// /// 要接入的程序集集合。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { ArgumentNullException.ThrowIfNull(assemblies); @@ -803,6 +807,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) GetServicesUnsafe.Clear(); _registeredInstances.Clear(); + _registeredCqrsHandlerAssemblyKeys.Clear(); _provider = null; _logger.Info("Container cleared"); } From 0cd1e9e83a9c444027bd56196680694a02649d3b Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:47:22 +0800 Subject: [PATCH 17/70] =?UTF-8?q?feat(ci):=20=E6=B7=BB=E5=8A=A0CI/CD?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=92=8CCQRS=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置CI构建和测试工作流,支持多.NET版本和并发测试 - 添加CodeQL静态代码分析工作流 - 实现自动版本递增和标签创建工作流 - 定义CQRS命令接口规范,包括响应式和流式命令 - 为架构测试添加空值参数异常文档注释 --- .github/workflows/auto-tag.yml | 21 +++++++++++-------- .github/workflows/ci.yml | 6 +++--- .github/workflows/codeql.yml | 8 +++---- ...ArchitectureAdditionalCqrsHandlersTests.cs | 3 +++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 45b9b112..a8e761bd 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -5,6 +5,8 @@ on: workflows: ["CI - Build & Test"] types: - completed + branches: + - main workflow_dispatch: concurrency: group: auto-tag-main @@ -13,15 +15,16 @@ concurrency: jobs: auto-tag: if: > - github.ref == 'refs/heads/main' && ( - ( - github.event_name == 'workflow_run' && - github.event.workflow_run.conclusion == 'success' && - contains(github.event.workflow_run.head_commit.message, '[release ci]') - ) - || - github.event_name == 'workflow_dispatch' + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' && + contains(github.event.workflow_run.head_commit.message, '[release ci]') + ) + || + ( + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/main' ) runs-on: ubuntu-latest @@ -61,4 +64,4 @@ jobs: fi git tag -a "$TAG" -m "Auto tag $TAG" - git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG" \ No newline at end of file + git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2b53c25..14606c84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,12 @@ # CI/CD工作流配置:构建和测试.NET项目 -# 该工作流在push到main/master分支或创建pull request时触发 +# 该工作流在推送到任意分支或创建面向任意分支的 pull request 时触发 name: CI - Build & Test on: push: - branches: [ main, master ] + branches: [ '**' ] pull_request: - branches: [ main, master ] + branches: [ '**' ] permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1802de8f..1bbc0abd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,14 +4,14 @@ name: "CodeQL" # 触发事件配置 # 在以下情况下触发工作流: -# 1. 推送到main分支时 -# 2. 针对main分支的拉取请求时 +# 1. 推送到任意分支时 +# 2. 针对任意分支的拉取请求时 # 3. 每天凌晨2点执行一次 on: push: - branches: [ "main" ] + branches: [ '**' ] pull_request: - branches: [ "main" ] + branches: [ '**' ] schedule: - cron: '0 2 * * *' diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index 23725e7c..d757beaa 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -168,6 +168,9 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl /// /// 目标服务集合。 /// 日志记录器。 + /// + /// 当 时抛出。 + /// public void Register(IServiceCollection services, ILogger logger) { ArgumentNullException.ThrowIfNull(services); From 340b6cae90eb06cdc17233d56ef67fc2f321c915 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:51:59 +0800 Subject: [PATCH 18/70] =?UTF-8?q?chore(ci):=20=E6=9B=B4=E6=96=B0GitHub=20A?= =?UTF-8?q?ctions=E5=B7=A5=E4=BD=9C=E6=B5=81=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除push触发器,仅保留pull request触发CI构建测试 - 添加CodeQL静态代码分析工作流,支持安全漏洞检测 - 配置每日凌晨2点定时执行CodeQL分析 - 设置.NET 8.0.x运行时环境支持 - 启用C#语言自动构建模式进行代码扫描 --- .github/workflows/ci.yml | 4 +--- .github/workflows/codeql.yml | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14606c84..1bb32d49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,8 @@ # CI/CD工作流配置:构建和测试.NET项目 -# 该工作流在推送到任意分支或创建面向任意分支的 pull request 时触发 +# 该工作流仅在创建或更新面向任意分支的 pull request 时触发 name: CI - Build & Test on: - push: - branches: [ '**' ] pull_request: branches: [ '**' ] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1bbc0abd..0a068113 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,12 +4,9 @@ name: "CodeQL" # 触发事件配置 # 在以下情况下触发工作流: -# 1. 推送到任意分支时 -# 2. 针对任意分支的拉取请求时 -# 3. 每天凌晨2点执行一次 +# 1. 针对任意分支的拉取请求时 +# 2. 每天凌晨2点执行一次 on: - push: - branches: [ '**' ] pull_request: branches: [ '**' ] schedule: From 49df81e46f83d86dd7f3f9d68fbe7c4d440f99b2 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:59:12 +0800 Subject: [PATCH 19/70] =?UTF-8?q?refactor(tests):=20=E9=87=8D=E6=9E=84=20C?= =?UTF-8?q?QRS=20=E5=A4=84=E7=90=86=E7=A8=8B=E5=BA=8F=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除自定义测试架构类,改用现有的 SyncTestArchitecture - 将 RegisterCqrsHandlersFromAssembly 测试方法中的架构创建逻辑提取为统一方法 - 更新重复程序集注册去重测试,验证不同 Assembly 实例但相同程序集键的情况 - 简化测试架构初始化逻辑,使用 AddPostRegistrationHook 替代自定义配置 - 调整注释文档以反映新的测试架构创建方式 - 移除 GitHub 工作流中对 main 分支的限制条件 --- .github/workflows/auto-tag.yml | 1 - ...ArchitectureAdditionalCqrsHandlersTests.cs | 34 +++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index a8e761bd..9588c304 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -18,7 +18,6 @@ jobs: ( github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_branch == 'main' && contains(github.event.workflow_run.head_commit.message, '[release ci]') ) || diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index d757beaa..5cb6206c 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -12,6 +12,8 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public sealed class ArchitectureAdditionalCqrsHandlersTests { + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + /// /// 初始化日志工厂和共享测试状态。 /// @@ -37,8 +39,6 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests "LoggerFactoryResolver.Provider should be captured during setup."); } - private ILoggerFactoryProvider? _previousLoggerFactoryProvider; - /// /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 /// @@ -46,7 +46,7 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly() { var generatedAssembly = CreateGeneratedHandlerAssembly(); - var architecture = new AdditionalHandlersTestArchitecture(target => + var architecture = CreateArchitecture(target => target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object)); await architecture.InitializeAsync(); @@ -63,16 +63,17 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests } /// - /// 验证同一额外程序集被重复声明时,不会向容器重复写入相同 handler 映射。 + /// 验证不同 实例只要解析到相同程序集键,就不会向容器重复写入相同 handler 映射。 /// [Test] public async Task RegisterCqrsHandlersFromAssembly_Should_Deduplicate_Repeated_Assembly_Registration() { - var generatedAssembly = CreateGeneratedHandlerAssembly(); - var architecture = new AdditionalHandlersTestArchitecture(target => + var generatedAssemblyA = CreateGeneratedHandlerAssembly(); + var generatedAssemblyB = CreateGeneratedHandlerAssembly(); + var architecture = CreateArchitecture(target => { - target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object); - target.RegisterCqrsHandlersFromAssemblies([generatedAssembly.Object]); + target.RegisterCqrsHandlersFromAssembly(generatedAssemblyA.Object); + target.RegisterCqrsHandlersFromAssemblies([generatedAssemblyB.Object]); }); await architecture.InitializeAsync(); @@ -106,18 +107,15 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests } /// - /// 用于测试额外程序集注册入口的最小架构实现。 + /// 创建复用现有测试架构基建的测试架构,并在注册阶段后执行额外程序集接入逻辑。 /// - private sealed class AdditionalHandlersTestArchitecture(Action configure) : - Architecture + /// 初始化阶段执行的额外 CQRS 程序集接入逻辑。 + /// 带有注册后钩子的测试架构实例。 + private static SyncTestArchitecture CreateArchitecture(Action configure) { - /// - /// 在初始化阶段执行测试注入的额外 CQRS 程序集接入逻辑。 - /// - protected override void OnInitialize() - { - configure(this); - } + var architecture = new SyncTestArchitecture(); + architecture.AddPostRegistrationHook(configure); + return architecture; } } From 2329cba3a604f2d3340b3b53d6dc570359b2f972 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:07:59 +0000 Subject: [PATCH 20/70] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- .../Architectures/ArchitectureAdditionalCqrsHandlersTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index 5cb6206c..04ee0571 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -42,6 +42,7 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests /// /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 /// + /// The asynchronous test task. [Test] public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly() { @@ -65,6 +66,7 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests /// /// 验证不同 实例只要解析到相同程序集键,就不会向容器重复写入相同 handler 映射。 /// + /// The asynchronous test task. [Test] public async Task RegisterCqrsHandlersFromAssembly_Should_Deduplicate_Repeated_Assembly_Registration() { @@ -195,4 +197,4 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl }); return handler.Object; } -} +} \ No newline at end of file From a01ec8d29cfbfad2ecfe8ea73788abf410e29e34 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:17:08 +0800 Subject: [PATCH 21/70] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8DPR=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E7=9A=84=E5=9F=BA=E7=BA=BF=E5=92=8C=E5=A4=B4=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将基础提交哈希从 github.event.before 更新为 github.event.pull_request.base.sha - 将当前提交哈希从 github.sha 更新为 github.event.pull_request.head.sha - 确保PR工作流正确比较基线和目标分支的差异 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bb32d49..c9bfcf89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,9 +67,9 @@ jobs: # 扫描路径,. 表示扫描整个仓库 path: . # 基础提交哈希,用于与当前提交进行比较 - base: ${{ github.event.before }} + base: ${{ github.event.pull_request.base.sha }} # 当前提交哈希,作为扫描的目标版本 - head: ${{ github.sha }} + head: ${{ github.event.pull_request.head.sha }} # 构建和测试 job(并行执行) build-and-test: From 048f96c6cdd72f2038d44b3f0b9c33f9f60a2048 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:41:53 +0800 Subject: [PATCH 22/70] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8A=E4=B8=8B=E6=96=87=E5=92=8CCQRS=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现ArchitectureContext提供系统、模型、工具等组件访问管理 - 添加CqrsDispatcher作为GFramework自有CQRS运行时分发器 - 集成Microsoft.Extensions.DependencyInjection作为IoC容器适配器 - 实现完整的命令、查询、事件处理机制 - 支持上下文感知处理器注入架构上下文 - 提供管道行为链处理机制 - 实现流式请求处理功能 - 添加服务实例缓存和优先级排序支持 --- .../Cqrs/ICqrsRuntime.cs | 51 ++++++++ .../GFramework.Core.Abstractions.csproj | 3 + GFramework.Core.Tests/CqrsTestRuntime.cs | 62 +++++++++ .../Ioc/MicrosoftDiContainerTests.cs | 9 +- .../Architectures/ArchitectureContext.cs | 31 ++--- .../Cqrs/Internal/CqrsDispatcher.cs | 122 ++++++++++-------- .../Internal/DefaultCqrsHandlerRegistrar.cs | 26 ++++ GFramework.Core/Ioc/MicrosoftDiContainer.cs | 25 +++- .../Services/Modules/CqrsRuntimeModule.cs | 61 +++++++++ .../Services/ServiceModuleManager.cs | 5 +- .../Cqrs/Command/ICommand.cs | 0 .../Cqrs/Command/ICommandInput.cs | 2 +- .../Cqrs/ICqrsHandlerRegistrar.cs | 17 +++ .../Cqrs/IInput.cs | 2 +- .../Cqrs/INotification.cs | 0 .../Cqrs/INotificationHandler.cs | 0 .../Cqrs/IPipelineBehavior.cs | 0 .../Cqrs/IRequest.cs | 0 .../Cqrs/IRequestHandler.cs | 0 .../Cqrs/IStreamRequest.cs | 0 .../Cqrs/IStreamRequestHandler.cs | 0 .../Cqrs/MessageHandlerDelegate.cs | 0 .../Cqrs/Notification/INotificationInput.cs | 2 +- .../Cqrs/Query/IQuery.cs | 0 .../Cqrs/Query/IQueryInput.cs | 2 +- .../Cqrs/Request/IRequestInput.cs | 2 +- .../Cqrs/Unit.cs | 0 .../Directory.Build.props | 18 +++ .../GFramework.Cqrs.Abstractions.csproj | 11 ++ GFramework.Cqrs.Abstractions/GlobalUsings.cs | 3 + GFramework.Cqrs/GFramework.Cqrs.csproj | 16 +++ GFramework.sln | 28 ++++ 32 files changed, 416 insertions(+), 82 deletions(-) create mode 100644 GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs create mode 100644 GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs create mode 100644 GFramework.Core/Services/Modules/CqrsRuntimeModule.cs rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Command/ICommand.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Command/ICommandInput.cs (84%) create mode 100644 GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IInput.cs (96%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/INotification.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/INotificationHandler.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IPipelineBehavior.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IRequest.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IRequestHandler.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IStreamRequest.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IStreamRequestHandler.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/MessageHandlerDelegate.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Notification/INotificationInput.cs (94%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Query/IQuery.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Query/IQueryInput.cs (79%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Request/IRequestInput.cs (95%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Unit.cs (100%) create mode 100644 GFramework.Cqrs.Abstractions/Directory.Build.props create mode 100644 GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj create mode 100644 GFramework.Cqrs.Abstractions/GlobalUsings.cs create mode 100644 GFramework.Cqrs/GFramework.Cqrs.csproj diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs new file mode 100644 index 00000000..e26310d7 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs @@ -0,0 +1,51 @@ +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义架构上下文使用的 CQRS runtime seam。 +/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦, +/// 使 不再直接依赖某个固定的 runtime 类型。 +/// +public interface ICqrsRuntime +{ + /// + /// 发送请求并返回响应。 + /// + /// 响应类型。 + /// 当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。 + /// 要分发的请求。 + /// 取消令牌。 + /// 请求响应。 + ValueTask SendAsync( + IArchitectureContext context, + IRequest request, + CancellationToken cancellationToken = default); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 要发布的通知。 + /// 取消令牌。 + /// 表示通知分发完成的值任务。 + ValueTask PublishAsync( + IArchitectureContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification; + + /// + /// 创建流式请求的异步响应序列。 + /// + /// 流元素类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 流式请求。 + /// 取消令牌。 + /// 按需生成的异步响应序列。 + IAsyncEnumerable CreateStream( + IArchitectureContext context, + IStreamRequest request, + CancellationToken cancellationToken = default); +} diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index 84d53f63..7a70b332 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -17,6 +17,9 @@ + + + all diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs index e9664925..f2f64f9b 100644 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ b/GFramework.Core.Tests/CqrsTestRuntime.cs @@ -1,4 +1,5 @@ using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; @@ -38,6 +39,65 @@ internal static class CqrsTestRuntime ?? throw new InvalidOperationException( "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsDispatcher", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher type."); + + private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher constructor."); + + private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar type."); + + private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = + DefaultCqrsHandlerRegistrarType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar constructor."); + + /// + /// 为裸测试容器补齐默认 CQRS runtime seam。 + /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, + /// 而无需完整启动服务模块管理器。 + /// + /// 目标测试容器。 + internal static void RegisterInfrastructure(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + var registrar = + (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + + container.Register(runtime); + container.Register(registrar); + } + /// /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 /// @@ -48,6 +108,8 @@ internal static class CqrsTestRuntime ArgumentNullException.ThrowIfNull(container); ArgumentNullException.ThrowIfNull(assemblies); + RegisterInfrastructure(container); + var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); RegisterHandlersMethod.Invoke( null, diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index b4e5af67..7126a3ad 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,6 +1,5 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; @@ -14,6 +13,8 @@ namespace GFramework.Core.Tests.Ioc; [TestFixture] public class MicrosoftDiContainerTests { + private MicrosoftDiContainer _container = null!; + /// /// 在每个测试方法执行前进行设置 /// @@ -29,9 +30,9 @@ public class MicrosoftDiContainerTests BindingFlags.NonPublic | BindingFlags.Instance); loggerField?.SetValue(_container, LoggerFactoryResolver.Provider.CreateLogger(nameof(MicrosoftDiContainer))); - } - private MicrosoftDiContainer _container = null!; + CqrsTestRuntime.RegisterInfrastructure(_container); + } /// /// 测试注册单例实例的功能 @@ -328,6 +329,8 @@ public class MicrosoftDiContainerTests descriptor.ServiceType == typeof(INotificationHandler)), Is.False); + // Clear 会移除测试手工补齐的 CQRS seam,需要先恢复基础设施再验证程序集去重状态是否已重置。 + CqrsTestRuntime.RegisterInfrastructure(_container); _container.RegisterCqrsHandlersFromAssembly(assembly); Assert.That( diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 77c04fcf..63bb34e8 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -2,18 +2,13 @@ using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using GFramework.Core.Cqrs.Internal; -using GFramework.Core.Logging; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Architectures; @@ -25,15 +20,15 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext { private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); private readonly ConcurrentDictionary _serviceCache = new(); - private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContext)); - private CqrsDispatcher? _cqrsDispatcher; + private ICqrsRuntime? _cqrsRuntime; #region CQRS Integration /// - /// 获取 CQRS 运行时分发器(延迟初始化)。 + /// 获取 CQRS runtime seam(延迟初始化)。 /// - private CqrsDispatcher CqrsDispatcher => _cqrsDispatcher ??= new CqrsDispatcher(_container, this, _logger); + private ICqrsRuntime CqrsRuntime => _cqrsRuntime ??= + _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 @@ -73,7 +68,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await CqrsDispatcher.SendAsync(request, cancellationToken); + return await CqrsRuntime.SendAsync(this, request, cancellationToken); } /// @@ -100,7 +95,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext where TNotification : INotification { ArgumentNullException.ThrowIfNull(notification); - await CqrsDispatcher.PublishAsync(notification, cancellationToken); + await CqrsRuntime.PublishAsync(this, notification, cancellationToken); } /// @@ -115,7 +110,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return CqrsDispatcher.CreateStream(request, cancellationToken); + return CqrsRuntime.CreateStream(this, request, cancellationToken); } /// @@ -151,7 +146,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询结果类型 /// 要发送的查询 /// 查询结果 - public TResult SendQuery(Abstractions.Query.IQuery query) + public TResult SendQuery(IQuery query) { if (query == null) throw new ArgumentNullException(nameof(query)); var queryBus = GetOrCache(); @@ -165,7 +160,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(GFramework.Core.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -191,7 +186,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(GFramework.Core.Abstractions.Cqrs.Query.IQuery query, + public async ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -327,7 +322,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的命令对象 /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask - public async ValueTask SendCommandAsync(GFramework.Core.Abstractions.Cqrs.Command.ICommand command, + public async ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -366,7 +361,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(GFramework.Core.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } @@ -388,7 +383,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令执行结果类型 /// 要发送的命令 /// 命令执行结果 - public TResult SendCommand(Abstractions.Command.ICommand command) + public TResult SendCommand(ICommand command) { ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs index 69f6794d..f1950cf7 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs @@ -14,34 +14,70 @@ namespace GFramework.Core.Cqrs.Internal; /// internal sealed class CqrsDispatcher( IIocContainer container, - IArchitectureContext context, - ILogger logger) + ILogger logger) : ICqrsRuntime { - private delegate ValueTask RequestInvoker(object handler, object request, CancellationToken cancellationToken); - private delegate ValueTask RequestPipelineInvoker( - object handler, - IReadOnlyList behaviors, - object request, - CancellationToken cancellationToken); - private delegate ValueTask NotificationInvoker(object handler, object notification, CancellationToken cancellationToken); - private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> + RequestInvokers = new(); + + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> + RequestPipelineInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> RequestInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> RequestPipelineInvokers = new(); private static readonly ConcurrentDictionary NotificationInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new(); + + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = + new(); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 通知对象。 + /// 取消令牌。 + public async ValueTask PublishAsync( + IArchitectureContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(notification); + + var notificationType = notification.GetType(); + var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); + var handlers = container.GetAll(handlerType); + + if (handlers.Count == 0) + { + logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); + return; + } + + var invoker = NotificationInvokers.GetOrAdd( + notificationType, + CreateNotificationInvoker); + + foreach (var handler in handlers) + { + PrepareHandler(handler, context); + await invoker(handler, notification, cancellationToken); + } + } /// /// 发送请求并返回结果。 /// /// 响应类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 /// 请求对象。 /// 取消令牌。 /// 请求响应。 public async ValueTask SendAsync( + IArchitectureContext context, IRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); @@ -50,12 +86,12 @@ internal sealed class CqrsDispatcher( ?? throw new InvalidOperationException( $"No CQRS request handler registered for {requestType.FullName}."); - PrepareHandler(handler); + PrepareHandler(handler, context); var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)); var behaviors = container.GetAll(behaviorType); foreach (var behavior in behaviors) - PrepareHandler(behavior); + PrepareHandler(behavior, context); if (behaviors.Count == 0) { @@ -75,51 +111,20 @@ internal sealed class CqrsDispatcher( return pipelineResult is null ? default! : (TResponse)pipelineResult; } - /// - /// 发布通知到所有已注册处理器。 - /// - /// 通知类型。 - /// 通知对象。 - /// 取消令牌。 - public async ValueTask PublishAsync( - TNotification notification, - CancellationToken cancellationToken = default) - where TNotification : INotification - { - ArgumentNullException.ThrowIfNull(notification); - - var notificationType = notification.GetType(); - var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); - var handlers = container.GetAll(handlerType); - - if (handlers.Count == 0) - { - logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); - return; - } - - var invoker = NotificationInvokers.GetOrAdd( - notificationType, - CreateNotificationInvoker); - - foreach (var handler in handlers) - { - PrepareHandler(handler); - await invoker(handler, notification, cancellationToken); - } - } - /// /// 创建流式请求并返回异步响应序列。 /// /// 响应元素类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 /// 流式请求对象。 /// 取消令牌。 /// 异步响应序列。 public IAsyncEnumerable CreateStream( + IArchitectureContext context, IStreamRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); @@ -128,7 +133,7 @@ internal sealed class CqrsDispatcher( ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); - PrepareHandler(handler); + PrepareHandler(handler, context); var invoker = StreamInvokers.GetOrAdd( (requestType, typeof(TResponse)), @@ -141,7 +146,8 @@ internal sealed class CqrsDispatcher( /// 为上下文感知处理器注入当前架构上下文。 /// /// 处理器实例。 - private void PrepareHandler(object handler) + /// 当前架构上下文。 + private static void PrepareHandler(object handler, IArchitectureContext context) { if (handler is IContextAware contextAware) contextAware.SetContext(context); @@ -260,4 +266,18 @@ internal sealed class CqrsDispatcher( var typedRequest = (TRequest)request; return typedHandler.Handle(typedRequest, cancellationToken); } + + private delegate ValueTask RequestInvoker(object handler, object request, + CancellationToken cancellationToken); + + private delegate ValueTask RequestPipelineInvoker( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken); + + private delegate ValueTask NotificationInvoker(object handler, object notification, + CancellationToken cancellationToken); + + private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); } diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs new file mode 100644 index 00000000..5d59f8a6 --- /dev/null +++ b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Cqrs.Internal; + +/// +/// 默认的 CQRS 处理器注册器实现。 +/// 该适配器把容器公开的 handler 接入入口转发到现有的注册流水线, +/// 使容器主路径只依赖 抽象。 +/// +internal sealed class DefaultCqrsHandlerRegistrar(IIocContainer container, ILogger logger) : ICqrsHandlerRegistrar +{ + private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// 按当前 runtime 约定扫描并注册处理器程序集。 + /// + /// 要接入的程序集集合。 + public void RegisterHandlers(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + CqrsHandlerRegistrar.RegisterHandlers(_container, assemblies, _logger); + } +} diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 712a41c1..c094647d 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,11 +1,9 @@ using System.ComponentModel; using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; -using GFramework.Core.Cqrs.Internal; using GFramework.Core.Logging; using GFramework.Core.Rule; @@ -424,7 +422,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) continue; } - CqrsHandlerRegistrar.RegisterHandlers(this, [assembly], _logger); + ResolveCqrsHandlerRegistrar().RegisterHandlers([assembly]); _registeredCqrsHandlerAssemblyKeys.Add(assemblyKey); } } @@ -456,6 +454,27 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) #region Get + /// + /// 获取当前容器中已注册的 CQRS 处理器注册器。 + /// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定, + /// 避免在容器未冻结前依赖完整的服务提供者构建流程。 + /// + /// 已注册的 CQRS 处理器注册器实例。 + /// 未找到可用的 CQRS 处理器注册器实例时抛出。 + private ICqrsHandlerRegistrar ResolveCqrsHandlerRegistrar() + { + var descriptor = GetServicesUnsafe.LastOrDefault(static service => + service.ServiceType == typeof(ICqrsHandlerRegistrar)); + + if (descriptor?.ImplementationInstance is ICqrsHandlerRegistrar registrar) + return registrar; + + const string errorMessage = + "ICqrsHandlerRegistrar not registered. Ensure the CQRS runtime module has been installed before registering handlers."; + _logger.Error(errorMessage); + throw new InvalidOperationException(errorMessage); + } + /// /// 获取指定泛型类型的服务实例 /// 返回第一个匹配的注册实例,如果不存在则返回null diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs new file mode 100644 index 00000000..915e07e2 --- /dev/null +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -0,0 +1,61 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Cqrs.Internal; +using GFramework.Core.Logging; + +namespace GFramework.Core.Services.Modules; + +/// +/// CQRS runtime 模块,用于把默认请求分发器与处理器注册器接入架构容器。 +/// 该模块在架构初始化早期完成注册,保证用户初始化阶段即可使用 CQRS 入口与 handler 自动接入能力。 +/// +public sealed class CqrsRuntimeModule : IServiceModule +{ + /// + /// 获取模块名称。 + /// + public string ModuleName => nameof(CqrsRuntimeModule); + + /// + /// 获取模块优先级。 + /// CQRS runtime 需要先于架构默认 handler 扫描路径可用,因此放在基础总线模块之后、用户初始化之前注册。 + /// + public int Priority => 15; + + /// + /// 获取模块启用状态,默认启用。 + /// + public bool IsEnabled => true; + + /// + /// 注册默认 CQRS runtime seam 实现。 + /// + /// 目标依赖注入容器。 + public void Register(IIocContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsDispatcher)); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(DefaultCqrsHandlerRegistrar)); + + container.Register(new CqrsDispatcher(container, dispatcherLogger)); + container.Register(new DefaultCqrsHandlerRegistrar(container, registrarLogger)); + } + + /// + /// 初始化模块。 + /// + public void Initialize() + { + } + + /// + /// 异步销毁模块。 + /// + /// 已完成的值任务。 + public ValueTask DestroyAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core/Services/ServiceModuleManager.cs b/GFramework.Core/Services/ServiceModuleManager.cs index d07c128b..a3965f4d 100644 --- a/GFramework.Core/Services/ServiceModuleManager.cs +++ b/GFramework.Core/Services/ServiceModuleManager.cs @@ -42,7 +42,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager /// /// 注册内置服务模块,并根据优先级排序后完成服务注册。 - /// 内置模块包括事件总线、命令执行器、查询执行器等核心模块。 + /// 内置模块包括事件总线、命令执行器、CQRS runtime、查询执行器等核心模块。 /// 同时注册通过 ArchitectureModuleRegistry 自动注册的外部模块。 /// /// IoC容器实例,用于模块服务注册。 @@ -57,6 +57,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager // 注册内置模块 RegisterModule(new EventBusModule()); RegisterModule(new CommandExecutorModule()); + RegisterModule(new CqrsRuntimeModule()); RegisterModule(new QueryExecutorModule()); RegisterModule(new AsyncQueryExecutorModule()); @@ -148,4 +149,4 @@ public sealed class ServiceModuleManager : IServiceModuleManager _builtInModulesRegistered = false; _logger.Info("All service modules destroyed"); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs similarity index 84% rename from GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs index 5ec607e5..a00d67e6 100644 --- a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs @@ -4,4 +4,4 @@ /// 命令输入接口,定义命令模式中输入数据的契约 /// 该接口作为标记接口使用,不包含任何成员定义 /// -public interface ICommandInput : IInput; \ No newline at end of file +public interface ICommandInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs new file mode 100644 index 00000000..0257b3ec --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义 CQRS 处理器程序集接入的 runtime seam。 +/// 该抽象负责承接“生成注册器优先、反射扫描回退”的处理器注册流程, +/// 让容器与架构启动链不再直接依赖固定的注册实现类型。 +/// +public interface ICqrsHandlerRegistrar +{ + /// + /// 扫描并注册指定程序集集合中的 CQRS 处理器。 + /// + /// 要接入的程序集集合。 + void RegisterHandlers(IEnumerable assemblies); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs similarity index 96% rename from GFramework.Core.Abstractions/Cqrs/IInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IInput.cs index dfed5012..ddf00622 100644 --- a/GFramework.Core.Abstractions/Cqrs/IInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs @@ -17,4 +17,4 @@ namespace GFramework.Core.Abstractions.Cqrs; /// 表示输入数据的标记接口。 /// 该接口用于标识各类CQRS模式中的输入参数类型。 /// -public interface IInput; \ No newline at end of file +public interface IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/INotification.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/INotification.cs rename to GFramework.Cqrs.Abstractions/Cqrs/INotification.cs diff --git a/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IRequest.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs diff --git a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs rename to GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs diff --git a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs similarity index 94% rename from GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs index 8b791839..236b30ff 100644 --- a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs @@ -17,4 +17,4 @@ namespace GFramework.Core.Abstractions.Cqrs.Notification; /// 表示通知输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中通知类型的输入参数。 /// -public interface INotificationInput : IInput; \ No newline at end of file +public interface INotificationInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs similarity index 79% rename from GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs index c505c4ff..7e0a5b4f 100644 --- a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs @@ -3,4 +3,4 @@ /// /// 查询输入接口,定义了查询操作的输入规范 /// -public interface IQueryInput : IInput; \ No newline at end of file +public interface IQueryInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs similarity index 95% rename from GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs index 0a0b1591..7b7ff83c 100644 --- a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs @@ -17,4 +17,4 @@ namespace GFramework.Core.Abstractions.Cqrs.Request; /// 表示请求输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中请求类型的输入参数。 /// -public interface IRequestInput : IInput; \ No newline at end of file +public interface IRequestInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Unit.cs b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/Unit.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Unit.cs diff --git a/GFramework.Cqrs.Abstractions/Directory.Build.props b/GFramework.Cqrs.Abstractions/Directory.Build.props new file mode 100644 index 00000000..8febdc42 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Directory.Build.props @@ -0,0 +1,18 @@ + + + netstandard2.1 + true + true + preview + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj new file mode 100644 index 00000000..8e07fd2c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + GeWuYou.$(AssemblyName) + true + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute + enable + true + + + diff --git a/GFramework.Cqrs.Abstractions/GlobalUsings.cs b/GFramework.Cqrs.Abstractions/GlobalUsings.cs new file mode 100644 index 00000000..5cd04a4e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/GFramework.Cqrs/GFramework.Cqrs.csproj b/GFramework.Cqrs/GFramework.Cqrs.csproj new file mode 100644 index 00000000..9f002283 --- /dev/null +++ b/GFramework.Cqrs/GFramework.Cqrs.csproj @@ -0,0 +1,16 @@ + + + + GeWuYou.$(AssemblyName) + net8.0;net9.0;net10.0 + disable + enable + true + true + + + + + + + diff --git a/GFramework.sln b/GFramework.sln index 6d4eb45f..67d53c69 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -38,6 +38,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGene EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstractions", "GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj", "{69C06523-98AA-49DE-95D4-4BF203716DD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramework.Cqrs\GFramework.Cqrs.csproj", "{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -276,6 +280,30 @@ Global {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 34e140e91997b78614e4124abc28053c99e8bcb0 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:13:43 +0800 Subject: [PATCH 23/70] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0=20Microso?= =?UTF-8?q?ft=20DI=20=E5=AE=B9=E5=99=A8=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E5=92=8C=20CI/CD=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 MicrosoftDiContainer 类,提供 Microsoft.Extensions.DependencyInjection 的适配器 - 添加 DefaultCqrsHandlerRegistrar 默认 CQRS 处理器注册器实现 - 配置 GitHub Actions CI/CD 工作流,包含代码质量检查和构建测试任务 - 设置 .NET 8/9/10 多版本支持和缓存策略 - 添加单元测试覆盖 IoC 容器的各项功能,包括注册、解析和生命周期管理 - 实现线程安全的读写锁机制保护容器操作 - 支持 CQRS 处理器和管道行为的注册管理 --- .github/workflows/ci.yml | 6 + .../Cqrs/ContainerRegistrationFixtures.cs | 36 ++++++ .../Ioc/MicrosoftDiContainerTests.cs | 2 +- .../Internal/DefaultCqrsHandlerRegistrar.cs | 1 + GFramework.Core/GFramework.Core.csproj | 1 + GFramework.Core/Ioc/MicrosoftDiContainer.cs | 1 + .../Coroutine/CqrsCoroutineExtensionsTests.cs | 0 .../Cqrs/CqrsHandlerRegistrarTests.cs | 1 - GFramework.Cqrs.Tests/CqrsTestRuntime.cs | 117 ++++++++++++++++++ .../GFramework.Cqrs.Tests.csproj | 26 ++++ GFramework.Cqrs.Tests/GlobalUsings.cs | 26 ++++ GFramework.Cqrs.Tests/Logging/TestLogger.cs | 56 +++++++++ .../Mediator/MediatorAdvancedFeaturesTests.cs | 2 - .../MediatorArchitectureIntegrationTests.cs | 2 - .../Mediator/MediatorComprehensiveTests.cs | 3 - GFramework.csproj | 9 ++ GFramework.sln | 14 +++ 17 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs rename {GFramework.Core.Tests => GFramework.Cqrs.Tests}/Coroutine/CqrsCoroutineExtensionsTests.cs (100%) rename {GFramework.Core.Tests => GFramework.Cqrs.Tests}/Cqrs/CqrsHandlerRegistrarTests.cs (99%) create mode 100644 GFramework.Cqrs.Tests/CqrsTestRuntime.cs create mode 100644 GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj create mode 100644 GFramework.Cqrs.Tests/GlobalUsings.cs create mode 100644 GFramework.Cqrs.Tests/Logging/TestLogger.cs rename {GFramework.Core.Tests => GFramework.Cqrs.Tests}/Mediator/MediatorAdvancedFeaturesTests.cs (99%) rename {GFramework.Core.Tests => GFramework.Cqrs.Tests}/Mediator/MediatorArchitectureIntegrationTests.cs (99%) rename {GFramework.Core.Tests => GFramework.Cqrs.Tests}/Mediator/MediatorComprehensiveTests.cs (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9bfcf89..defe6c71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,12 @@ jobs: --logger "trx;LogFileName=sg-$RANDOM.trx" \ --results-directory TestResults & + dotnet test GFramework.Cqrs.Tests \ + -c Release \ + --no-build \ + --logger "trx;LogFileName=cqrs-$RANDOM.trx" \ + --results-directory TestResults & + dotnet test GFramework.Ecs.Arch.Tests \ -c Release \ --no-build \ diff --git a/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs new file mode 100644 index 00000000..f01af4de --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Core.Tests.Cqrs; + +/// +/// 为容器层测试提供可扫描的最小通知夹具。 +/// +internal sealed record DeterministicOrderNotification : INotification; + +/// +/// 供容器注册测试验证程序集扫描结果的通知处理器。 +/// +internal sealed class DeterministicOrderNotificationHandler : INotificationHandler +{ + /// + /// 无副作用地消费通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 7126a3ad..6ae0747c 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -315,7 +315,7 @@ public class MicrosoftDiContainerTests [Test] public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State() { - var assembly = typeof(CqrsHandlerRegistrarTests).Assembly; + var assembly = typeof(DeterministicOrderNotification).Assembly; _container.RegisterCqrsHandlersFromAssembly(assembly); Assert.That( diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs index 5d59f8a6..dca76290 100644 --- a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -1,4 +1,5 @@ using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index c450b44c..f3e41eab 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -9,6 +9,7 @@ true + diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index c094647d..49ce44f2 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs similarity index 100% rename from GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs rename to GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs similarity index 99% rename from GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs rename to GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 7748227c..05a8b0c7 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -1,4 +1,3 @@ -using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; diff --git a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs b/GFramework.Cqrs.Tests/CqrsTestRuntime.cs new file mode 100644 index 00000000..f9bb143b --- /dev/null +++ b/GFramework.Cqrs.Tests/CqrsTestRuntime.cs @@ -0,0 +1,117 @@ +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Architectures; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; + +namespace GFramework.Core.Tests; + +/// +/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 +/// +/// +/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, +/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 +/// +internal static class CqrsTestRuntime +{ + private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar type."); + + private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType + .GetMethod( + "RegisterHandlers", + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static, + binder: null, + [ + typeof(IIocContainer), + typeof(IEnumerable), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + + private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsDispatcher", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher type."); + + private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher constructor."); + + private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar type."); + + private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = + DefaultCqrsHandlerRegistrarType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar constructor."); + + /// + /// 为裸测试容器补齐默认 CQRS runtime seam。 + /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, + /// 而无需完整启动服务模块管理器。 + /// + /// 目标测试容器。 + internal static void RegisterInfrastructure(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + var registrar = + (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + + container.Register(runtime); + container.Register(registrar); + } + + /// + /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 + /// + /// 承载处理器映射的测试容器。 + /// 要扫描的程序集集合。 + internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(assemblies); + + RegisterInfrastructure(container); + + var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + RegisterHandlersMethod.Invoke( + null, + [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]); + } +} diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj new file mode 100644 index 00000000..acbcc536 --- /dev/null +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + $(TestTargetFrameworks) + disable + enable + false + true + 0 + + + + + + + + + + + + + + + + diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs new file mode 100644 index 00000000..d31630ed --- /dev/null +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +global using System; +global using System.Collections; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Linq; +global using System.Reflection; +global using System.Runtime.CompilerServices; +global using System.Threading; +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; +global using Moq; +global using NUnit.Compatibility; +global using NUnit.Framework; diff --git a/GFramework.Cqrs.Tests/Logging/TestLogger.cs b/GFramework.Cqrs.Tests/Logging/TestLogger.cs new file mode 100644 index 00000000..aaf65d22 --- /dev/null +++ b/GFramework.Cqrs.Tests/Logging/TestLogger.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace GFramework.Core.Tests.Logging; + +/// +/// 供 CQRS 测试项目复用的最小日志记录器实现。 +/// +public sealed class TestLogger : AbstractLogger +{ + /// + /// 初始化测试日志记录器。 + /// + /// 日志名称。 + /// 最小日志级别。 + public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base(name, minLevel) + { + } + + /// + /// 获取当前测试期间捕获到的日志条目。 + /// + public List Logs { get; } = []; + + /// + /// 将日志写入内存,供断言使用。 + /// + /// 日志级别。 + /// 日志消息。 + /// 关联异常。 + protected override void Write(LogLevel level, string message, Exception? exception) + { + Logs.Add(new LogEntry(level, message, exception)); + } + + /// + /// 表示单条测试日志记录。 + /// + /// 日志级别。 + /// 日志消息。 + /// 关联异常。 + public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception); +} diff --git a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs similarity index 99% rename from GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index 2dc2503a..cadb340b 100644 --- a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Ioc; diff --git a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs similarity index 99% rename from GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index e176cce5..7d73a45e 100644 --- a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; diff --git a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs similarity index 99% rename from GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index 27dfed5c..cdaf9af6 100644 --- a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -1,6 +1,3 @@ -using System.Diagnostics; -using System.Reflection; -using System.Runtime.CompilerServices; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Events; diff --git a/GFramework.csproj b/GFramework.csproj index 76f9a088..679cbf64 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -63,6 +63,9 @@ + + + @@ -104,6 +107,9 @@ + + + @@ -131,6 +137,9 @@ + + + diff --git a/GFramework.sln b/GFramework.sln index 67d53c69..088d63d3 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstraction EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramework.Cqrs\GFramework.Cqrs.csproj", "{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Tests", "GFramework.Cqrs.Tests\GFramework.Cqrs.Tests.csproj", "{29037A55-9A89-425C-AB33-D44872B2E601}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -304,6 +306,18 @@ Global {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.Build.0 = Release|Any CPU {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.ActiveCfg = Release|Any CPU {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 49ed5d0d0686e167ce0dccf7ccc02d54c9adbf01 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:22:00 +0800 Subject: [PATCH 24/70] =?UTF-8?q?refactor(tests):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E6=8A=BD=E8=B1=A1=E5=B1=82=E4=BE=9D=E8=B5=96=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ContainerRegistrationFixtures.cs中添加GFramework.Core.Abstractions.Cqrs命名空间引用 - 在MicrosoftDiContainerTests.cs中添加GFramework.Core.Abstractions.Cqrs命名空间引用 - 统一测试文件中的依赖注入配置 - 确保CQRS相关接口的正确引用路径 --- GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs | 2 ++ GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs index f01af4de..58483c15 100644 --- a/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs +++ b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using GFramework.Core.Abstractions.Cqrs; + namespace GFramework.Core.Tests.Cqrs; /// diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 6ae0747c..186248ff 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; @@ -13,8 +14,6 @@ namespace GFramework.Core.Tests.Ioc; [TestFixture] public class MicrosoftDiContainerTests { - private MicrosoftDiContainer _container = null!; - /// /// 在每个测试方法执行前进行设置 /// @@ -34,6 +33,8 @@ public class MicrosoftDiContainerTests CqrsTestRuntime.RegisterInfrastructure(_container); } + private MicrosoftDiContainer _container = null!; + /// /// 测试注册单例实例的功能 /// From 1c5c5c812ad7ccfc35e4c30e36eaafa96405a6a7 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:26:38 +0800 Subject: [PATCH 25/70] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0=20Mezia?= =?UTF-8?q?ntou.Analyzer=20=E5=92=8C=20Meziantou.Polyfill=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=8C=85=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Meziantou.Analyzer 从 2.0.264 版本升级到 3.0.46 版本 - 将 Meziantou.Polyfill 从 1.0.71 版本升级到 1.0.109 版本 --- GFramework.Cqrs.Abstractions/Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GFramework.Cqrs.Abstractions/Directory.Build.props b/GFramework.Cqrs.Abstractions/Directory.Build.props index 8febdc42..9f372d0c 100644 --- a/GFramework.Cqrs.Abstractions/Directory.Build.props +++ b/GFramework.Cqrs.Abstractions/Directory.Build.props @@ -6,11 +6,11 @@ preview - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers From 28cdf791dfed3274ba7e3193d33d055acc697297 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:27:14 +0800 Subject: [PATCH 26/70] =?UTF-8?q?refactor(tests):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=AD=A6=E5=91=8A=E7=BA=A7=E5=88=AB=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了 GFramework.Cqrs.Tests 项目的 WarningLevel 配置 - 使测试项目遵循默认警告级别设置 --- GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj index acbcc536..09cded18 100644 --- a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -7,7 +7,6 @@ enable false true - 0 From aba304f6675a12fb9c4c482f33fb045813025554 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:28:51 +0800 Subject: [PATCH 27/70] =?UTF-8?q?test(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 验证通知处理器按稳定名称顺序执行而非依赖反射枚举顺序 - 测试部分类型加载失败时保留可加载类型并记录诊断日志 - 验证源码生成注册器优先级高于反射扫描机制 - 测试生成注册器元数据损坏时回退到反射扫描路径 - 实现确定性通知处理器执行顺序验证功能 - 添加捕获型日志工厂提供程序用于测试断言 - 修正命名空间从Core.Tests.Logging到Cqrs.Tests.Logging --- GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs | 8 ++++---- GFramework.Cqrs.Tests/Logging/TestLogger.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 05a8b0c7..8c3aa9f7 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -3,7 +3,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using GFramework.Core.Tests.Logging; +using GFramework.Cqrs.Tests.Logging; namespace GFramework.Core.Tests.Cqrs; @@ -13,9 +13,6 @@ namespace GFramework.Core.Tests.Cqrs; [TestFixture] internal sealed class CqrsHandlerRegistrarTests { - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 初始化测试容器并重置共享状态。 /// @@ -45,6 +42,9 @@ internal sealed class CqrsHandlerRegistrarTests DeterministicNotificationHandlerState.Reset(); } + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// /// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。 /// diff --git a/GFramework.Cqrs.Tests/Logging/TestLogger.cs b/GFramework.Cqrs.Tests/Logging/TestLogger.cs index aaf65d22..c0432bc7 100644 --- a/GFramework.Cqrs.Tests/Logging/TestLogger.cs +++ b/GFramework.Cqrs.Tests/Logging/TestLogger.cs @@ -14,7 +14,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; -namespace GFramework.Core.Tests.Logging; +namespace GFramework.Cqrs.Tests.Logging; /// /// 供 CQRS 测试项目复用的最小日志记录器实现。 From ede8a8faa49479030bc82e970b4a11cdbc2dc3e4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:34:14 +0800 Subject: [PATCH 28/70] =?UTF-8?q?fix(namespace):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正Core模块命名空间 - 修正Godot模块命名空间 --- .../Architectures/IArchitectureContext.cs | 11 +++++---- .../Cqrs/ICqrsRuntime.cs | 1 + ...ArchitectureAdditionalCqrsHandlersTests.cs | 7 +++--- .../ArchitectureModulesBehaviorTests.cs | 12 +++++----- .../ArchitectureServicesTests.cs | 20 ++++++++-------- .../Architectures/GameContextTests.cs | 12 ++++++---- .../Command/AbstractAsyncCommandTests.cs | 4 ++-- .../Command/CommandExecutorTests.cs | 4 ++-- .../Cqrs/ContainerRegistrationFixtures.cs | 2 +- GFramework.Core.Tests/CqrsTestRuntime.cs | 1 + .../Ioc/MicrosoftDiContainerTests.cs | 2 +- .../Query/AbstractAsyncQueryTests.cs | 4 ++-- .../Query/AsyncQueryExecutorTests.cs | 4 ++-- .../Query/QueryExecutorTests.cs | 4 ++-- .../Architectures/ArchitectureContext.cs | 11 +++++---- .../Command/AbstractAsyncCommandWithInput.cs | 4 ++-- .../Command/AbstractAsyncCommandWithResult.cs | 4 ++-- .../Command/AbstractCommandWithInput.cs | 10 ++++---- .../Command/AbstractCommandWithResult.cs | 10 ++++---- GFramework.Core/Command/EmptyCommandInput.cs | 4 ++-- .../Extensions/CqrsCoroutineExtensions.cs | 5 ++-- .../Extensions/MediatorCoroutineExtensions.cs | 5 ++-- .../Cqrs/Behaviors/LoggingBehavior.cs | 2 +- .../Cqrs/Behaviors/PerformanceBehavior.cs | 2 +- .../Cqrs/Command/AbstractCommandHandler.cs | 4 ++-- .../Command/AbstractStreamCommandHandler.cs | 4 ++-- GFramework.Core/Cqrs/Command/CommandBase.cs | 2 +- .../Cqrs/Internal/CqrsDispatcher.cs | 1 + .../Cqrs/Internal/CqrsHandlerRegistrar.cs | 1 + .../Internal/DefaultCqrsHandlerRegistrar.cs | 2 +- .../AbstractNotificationHandler.cs | 2 +- .../Cqrs/Notification/NotificationBase.cs | 4 ++-- .../Cqrs/Query/AbstractQueryHandler.cs | 4 ++-- .../Cqrs/Query/AbstractStreamQueryHandler.cs | 4 ++-- GFramework.Core/Cqrs/Query/QueryBase.cs | 2 +- .../Cqrs/Request/AbstractRequestHandler.cs | 2 +- .../Request/AbstractStreamRequestHandler.cs | 2 +- GFramework.Core/Cqrs/Request/RequestBase.cs | 4 ++-- .../ContextAwareCqrsCommandExtensions.cs | 4 ++-- .../Extensions/ContextAwareCqrsExtensions.cs | 4 ++-- .../ContextAwareCqrsQueryExtensions.cs | 4 ++-- .../ContextAwareMediatorCommandExtensions.cs | 5 ++-- .../ContextAwareMediatorExtensions.cs | 5 ++-- .../ContextAwareMediatorQueryExtensions.cs | 5 ++-- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 2 +- .../Query/AbstractAsyncQueryWithResult.cs | 6 ++--- .../Query/AbstractQueryWithResult.cs | 8 +++---- GFramework.Core/Query/EmptyQueryInput.cs | 4 ++-- .../Services/Modules/CqrsRuntimeModule.cs | 1 + .../Cqrs/Command/ICommand.cs | 2 +- .../Cqrs/Command/ICommandInput.cs | 2 +- .../Cqrs/ICqrsHandlerRegistrar.cs | 2 +- GFramework.Cqrs.Abstractions/Cqrs/IInput.cs | 2 +- .../Cqrs/INotification.cs | 2 +- .../Cqrs/INotificationHandler.cs | 2 +- .../Cqrs/IPipelineBehavior.cs | 2 +- GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs | 2 +- .../Cqrs/IRequestHandler.cs | 2 +- .../Cqrs/IStreamRequest.cs | 2 +- .../Cqrs/IStreamRequestHandler.cs | 2 +- .../Cqrs/MessageHandlerDelegate.cs | 2 +- .../Cqrs/Notification/INotificationInput.cs | 2 +- .../Cqrs/Query/IQuery.cs | 2 +- .../Cqrs/Query/IQueryInput.cs | 2 +- .../Cqrs/Request/IRequestInput.cs | 2 +- GFramework.Cqrs.Abstractions/Cqrs/Unit.cs | 2 +- .../Coroutine/CqrsCoroutineExtensionsTests.cs | 5 ++-- .../Cqrs/CqrsHandlerRegistrarTests.cs | 3 ++- GFramework.Cqrs.Tests/CqrsTestRuntime.cs | 3 ++- .../Mediator/MediatorAdvancedFeaturesTests.cs | 12 +++++----- .../MediatorArchitectureIntegrationTests.cs | 14 +++++------ .../Mediator/MediatorComprehensiveTests.cs | 23 +++++++++---------- ...utoRegisterExportedCollectionsAttribute.cs | 2 +- .../UI/AutoSceneAttribute.cs | 2 +- .../UI/AutoUiPageAttribute.cs | 2 +- .../UI/RegisterExportedCollectionAttribute.cs | 2 +- .../ContextAwareCoroutineExtensions.cs | 10 ++++---- 77 files changed, 176 insertions(+), 167 deletions(-) diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index b9b5dc9a..0eb6a43f 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -1,11 +1,11 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Abstractions.Architectures; @@ -131,7 +131,7 @@ public interface IArchitectureContext /// /// 这是迁移后的推荐命令入口。无返回值命令应实现 IRequest<Unit>,并优先通过 调用。 /// - TResponse SendCommand(Cqrs.Command.ICommand command); + TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command); /// @@ -147,7 +147,8 @@ public interface IArchitectureContext /// 要发送的 CQRS 命令。 /// 取消令牌。 /// 包含命令执行结果的值任务。 - ValueTask SendCommandAsync(Cqrs.Command.ICommand command, + ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default); @@ -176,7 +177,7 @@ public interface IArchitectureContext /// /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse>。 /// - TResponse SendQuery(Cqrs.Query.IQuery query); + TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query); /// /// 异步发送一个旧版查询请求。 @@ -193,7 +194,7 @@ public interface IArchitectureContext /// 要发送的 CQRS 查询。 /// 取消令牌。 /// 包含查询结果的值任务。 - ValueTask SendQueryAsync(Cqrs.Query.IQuery query, + ValueTask SendQueryAsync(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default); /// diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs index e26310d7..d9efcb07 100644 --- a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs @@ -1,4 +1,5 @@ using GFramework.Core.Abstractions.Architectures; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Abstractions.Cqrs; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index 04ee0571..67255b41 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -3,6 +3,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -12,8 +13,6 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public sealed class ArchitectureAdditionalCqrsHandlersTests { - private ILoggerFactoryProvider? _previousLoggerFactoryProvider; - /// /// 初始化日志工厂和共享测试状态。 /// @@ -39,6 +38,8 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests "LoggerFactoryResolver.Provider should be captured during setup."); } + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + /// /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 /// @@ -197,4 +198,4 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl }); return handler.Object; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index ade4a1a7..cfe0db79 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -2,7 +2,7 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using GfCqrs = GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -151,14 +151,14 @@ public class ArchitectureModulesBehaviorTests /// /// 用于验证管道行为注册是否生效的测试请求。 /// -public sealed class ModuleBehaviorRequest : GfCqrs.IRequest +public sealed class ModuleBehaviorRequest : IRequest { } /// /// 处理测试请求的处理器。 /// -public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler +public sealed class ModuleBehaviorRequestHandler : IRequestHandler { /// /// 返回固定结果,便于聚焦验证管道行为是否执行。 @@ -177,8 +177,8 @@ public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler /// 请求类型。 /// 响应类型。 -public sealed class TrackingPipelineBehavior : GfCqrs.IPipelineBehavior - where TRequest : GfCqrs.IRequest +public sealed class TrackingPipelineBehavior : IPipelineBehavior + where TRequest : IRequest { /// /// 获取当前测试进程中该请求类型对应的行为触发次数。 @@ -193,7 +193,7 @@ public sealed class TrackingPipelineBehavior : GfCqrs.IPipe /// 取消令牌。 /// 下游处理器的响应结果。 public async ValueTask Handle( - TRequest message, GfCqrs.MessageHandlerDelegate next, + TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) { InvocationCount++; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs index 94b749b6..57dc19a5 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs @@ -1,6 +1,5 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -14,6 +13,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Architectures; @@ -34,10 +34,6 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureServicesTests { - private TestArchitectureContextV3? _context; - - private ArchitectureServices? _services; - [SetUp] public void SetUp() { @@ -45,6 +41,10 @@ public class ArchitectureServicesTests _context = new TestArchitectureContextV3(); } + private TestArchitectureContextV3? _context; + + private ArchitectureServices? _services; + private void RegisterBuiltInServices() { _services!.ModuleManager.RegisterBuiltInModules(_services.Container); @@ -359,24 +359,26 @@ public class TestArchitectureContextV3 : IArchitectureContext throw new NotImplementedException(); } - public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, + public ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, + public ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index 28e3cc97..9b990e78 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -1,6 +1,5 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -14,6 +13,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Architectures; @@ -428,7 +428,8 @@ public class TestArchitectureContext : IArchitectureContext /// 取消令牌。 /// 命令响应任务。 /// 该测试桩未实现此成员。 - public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, + public ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -441,7 +442,7 @@ public class TestArchitectureContext : IArchitectureContext /// 要发送的命令。 /// 命令响应。 /// 该测试桩未实现此成员。 - public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } @@ -454,7 +455,8 @@ public class TestArchitectureContext : IArchitectureContext /// 取消令牌。 /// 查询结果任务。 /// 该测试桩未实现此成员。 - public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, + public ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -467,7 +469,7 @@ public class TestArchitectureContext : IArchitectureContext /// 要发送的查询。 /// 查询结果。 /// 该测试桩未实现此成员。 - public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } diff --git a/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs b/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs index e7a0611e..2cc2ec13 100644 --- a/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs +++ b/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs @@ -1,5 +1,4 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Architectures; using GFramework.Core.Command; @@ -7,6 +6,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Tests.Command; @@ -396,4 +396,4 @@ public sealed class TestAsyncCommandWithResultChildV3 : AbstractAsyncCommand public int DoubleValue { get; init; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs index 6491946d..b23cc8f9 100644 --- a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs @@ -1,5 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -292,4 +292,4 @@ public sealed class TestAsyncQueryResult /// 获取或设置双倍值 /// public int DoubleValue { get; init; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Query/QueryExecutorTests.cs b/GFramework.Core.Tests/Query/QueryExecutorTests.cs index f0305b4b..a9cde117 100644 --- a/GFramework.Core.Tests/Query/QueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/QueryExecutorTests.cs @@ -1,5 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -121,4 +121,4 @@ public sealed class TestStringQuery : AbstractQuery { return $"Result: {input.Value * 2}"; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 63bb34e8..e52960bf 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -9,6 +9,7 @@ using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Architectures; @@ -160,7 +161,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -186,7 +187,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, + public async ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -322,7 +324,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的命令对象 /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask - public async ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, + public async ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -361,7 +364,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs index 491bbd8e..d97ab90b 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -26,4 +26,4 @@ public abstract class AbstractAsyncCommand(TInput input) : ContextAwareB /// 命令输入参数 /// 表示异步操作的任务 protected abstract Task OnExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs index 47390192..ae19458e 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -27,4 +27,4 @@ public abstract class AbstractAsyncCommand(TInput input) : Cont /// 命令输入参数 /// 表示异步操作且包含结果的任务 protected abstract Task OnExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractCommandWithInput.cs b/GFramework.Core/Command/AbstractCommandWithInput.cs index 7512c760..c3ebbb03 100644 --- a/GFramework.Core/Command/AbstractCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractCommandWithInput.cs @@ -1,6 +1,6 @@ -using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Command; @@ -9,13 +9,13 @@ namespace GFramework.Core.Command; /// /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand +public abstract class AbstractCommand(TInput input) : ContextAwareBase, ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法 /// - void GFramework.Core.Abstractions.Command.ICommand.Execute() + void ICommand.Execute() { OnExecute(input); } diff --git a/GFramework.Core/Command/AbstractCommandWithResult.cs b/GFramework.Core/Command/AbstractCommandWithResult.cs index 7ecc9522..ca40957a 100644 --- a/GFramework.Core/Command/AbstractCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractCommandWithResult.cs @@ -1,6 +1,5 @@ -using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -10,14 +9,15 @@ namespace GFramework.Core.Command; /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行后返回的结果类型 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand +public abstract class AbstractCommand(TInput input) + : ContextAwareBase, Abstractions.Command.ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法 /// /// 命令执行后的结果 - TResult GFramework.Core.Abstractions.Command.ICommand.Execute() + TResult Abstractions.Command.ICommand.Execute() { return OnExecute(input); } diff --git a/GFramework.Core/Command/EmptyCommandInput.cs b/GFramework.Core/Command/EmptyCommandInput.cs index 4d08f4e7..540c158e 100644 --- a/GFramework.Core/Command/EmptyCommandInput.cs +++ b/GFramework.Core/Command/EmptyCommandInput.cs @@ -1,4 +1,4 @@ -using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -9,4 +9,4 @@ namespace GFramework.Core.Command; /// 该类实现了ICommandInput接口,作为命令模式中的输入参数载体 /// 通常用于不需要额外输入参数的简单命令操作 /// -public sealed class EmptyCommandInput : ICommandInput; \ No newline at end of file +public sealed class EmptyCommandInput : ICommandInput; diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 98667656..ec2b55a0 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -1,10 +1,9 @@ using System.Runtime.ExceptionServices; using GFramework.Core.Abstractions.Coroutine; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Coroutine.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index f955f175..70e3c29f 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -13,15 +13,14 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Coroutine; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs index 4aaf797a..7230f53d 100644 --- a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs @@ -12,9 +12,9 @@ // limitations under the License. using System.Diagnostics; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Behaviors; diff --git a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs b/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs index 7fc266d0..35ab2978 100644 --- a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs +++ b/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs @@ -12,9 +12,9 @@ // limitations under the License. using System.Diagnostics; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Behaviors; diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs index 528de106..d7ebb117 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs index 847563c4..223a9cc5 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; diff --git a/GFramework.Core/Cqrs/Command/CommandBase.cs b/GFramework.Core/Cqrs/Command/CommandBase.cs index 78fa134e..d8232608 100644 --- a/GFramework.Core/Cqrs/Command/CommandBase.cs +++ b/GFramework.Core/Cqrs/Command/CommandBase.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs index f1950cf7..4e2c739d 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs @@ -5,6 +5,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Internal; diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs index 189ae8f0..65b1cbcf 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -2,6 +2,7 @@ using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Internal; diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs index dca76290..ddf0c06a 100644 --- a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -1,7 +1,7 @@ using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Internal; diff --git a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs b/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs index 1b1157ab..da6f5281 100644 --- a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs +++ b/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Notification; diff --git a/GFramework.Core/Cqrs/Notification/NotificationBase.cs b/GFramework.Core/Cqrs/Notification/NotificationBase.cs index f04488b9..05db2de7 100644 --- a/GFramework.Core/Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Core/Cqrs/Notification/NotificationBase.cs @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Notification; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Notification; namespace GFramework.Core.Cqrs.Notification; diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs index e9a3795f..85c86425 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; diff --git a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs index 015da1da..9695dc42 100644 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -12,8 +12,8 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Core/Cqrs/Query/QueryBase.cs index 6bccf549..759b8df1 100644 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ b/GFramework.Core/Cqrs/Query/QueryBase.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; diff --git a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs b/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs index 4ef6a270..f26c7cfa 100644 --- a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs +++ b/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; diff --git a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs b/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs index a15ed5d7..2cbf438d 100644 --- a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs +++ b/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; diff --git a/GFramework.Core/Cqrs/Request/RequestBase.cs b/GFramework.Core/Cqrs/Request/RequestBase.cs index ce85784f..5ff18a04 100644 --- a/GFramework.Core/Cqrs/Request/RequestBase.cs +++ b/GFramework.Core/Cqrs/Request/RequestBase.cs @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Request; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Request; namespace GFramework.Core.Cqrs.Request; diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs index b71669ee..f99fae25 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -1,7 +1,7 @@ -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs index ab09e689..6db156f5 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs @@ -1,7 +1,7 @@ -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 统一扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs index 9906bc3d..40ec0f7d 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -1,7 +1,7 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 85f9776e..41d9df27 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,13 +1,12 @@ using System.ComponentModel; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index 68b130ce..b4d607bc 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,13 +1,12 @@ using System.ComponentModel; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 统一接口扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index d7fada4a..49b57445 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,13 +1,12 @@ using System.ComponentModel; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 49ce44f2..dc14485e 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,12 +1,12 @@ using System.ComponentModel; using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Logging; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Ioc; diff --git a/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs b/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs index 03c35071..03713712 100644 --- a/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs @@ -1,6 +1,6 @@ -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Query; +using GFramework.Core.Abstractions.Query; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -30,4 +30,4 @@ public abstract class AbstractAsyncQuery( /// 查询输入参数 /// 返回查询结果的异步任务 protected abstract Task OnDoAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Query/AbstractQueryWithResult.cs b/GFramework.Core/Query/AbstractQueryWithResult.cs index 2c87622a..ae099abe 100644 --- a/GFramework.Core/Query/AbstractQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractQueryWithResult.cs @@ -1,6 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Query; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -9,7 +8,8 @@ namespace GFramework.Core.Query; /// /// 查询输入参数的类型,必须实现IQueryInput接口 /// 查询结果的类型 -public abstract class AbstractQuery(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery +public abstract class AbstractQuery(TInput input) + : ContextAwareBase, Abstractions.Query.IQuery where TInput : IQueryInput { /// diff --git a/GFramework.Core/Query/EmptyQueryInput.cs b/GFramework.Core/Query/EmptyQueryInput.cs index 7d707189..a6a0cd67 100644 --- a/GFramework.Core/Query/EmptyQueryInput.cs +++ b/GFramework.Core/Query/EmptyQueryInput.cs @@ -1,4 +1,4 @@ -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -8,4 +8,4 @@ namespace GFramework.Core.Query; /// /// 该类实现了IQueryInput接口,作为占位符使用,适用于那些不需要额外输入参数的查询场景 /// -public sealed class EmptyQueryInput : IQueryInput; \ No newline at end of file +public sealed class EmptyQueryInput : IQueryInput; diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 915e07e2..5ca7a909 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -3,6 +3,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Cqrs.Internal; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Services.Modules; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs index ba04331e..6eaa1869 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs.Command; +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; /// /// 表示一个 CQRS 命令。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs index a00d67e6..9f5be0b6 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs.Command; +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; /// /// 命令输入接口,定义命令模式中输入数据的契约 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs index 0257b3ec..39635d3f 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 定义 CQRS 处理器程序集接入的 runtime seam。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs index ddf00622..a8b6dbcb 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示输入数据的标记接口。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs index 9d69e28e..727f9c07 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示一个一对多发布的通知消息。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs index 23861d1d..e3a007c8 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示处理通知消息的处理器契约。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs index cd01aad3..11423c40 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 定义 CQRS 请求处理前后的管道行为。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs index 26259fc4..78627e4b 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示一个有响应的 CQRS 请求。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs index 2415e282..95cdd1d1 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示处理单个 CQRS 请求的处理器契约。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs index 05ffa5df..37a211d4 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示一个流式 CQRS 请求。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs index 1c6e02a7..44e7c79d 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示处理流式 CQRS 请求的处理器契约。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs index 520f9fee..8575ebd8 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示 CQRS 请求在管道中继续向下执行的处理委托。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs index 236b30ff..4fb6f735 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs.Notification; +namespace GFramework.Cqrs.Abstractions.Cqrs.Notification; /// /// 表示通知输入数据的标记接口。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs index cbb1586e..9592a9bd 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs.Query; +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; /// /// 表示一个 CQRS 查询。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs index 7e0a5b4f..a17e7b6a 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs.Query; +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; /// /// 查询输入接口,定义了查询操作的输入规范 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs index 7b7ff83c..14f89b89 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs.Request; +namespace GFramework.Cqrs.Abstractions.Cqrs.Request; /// /// 表示请求输入数据的标记接口。 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs index 7dc3da14..57d053bc 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示没有实际返回值的 CQRS 响应类型。 diff --git a/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index 67e9537d..bd41c1e4 100644 --- a/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -13,11 +13,10 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Coroutine; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Tests.Coroutine; +namespace GFramework.Cqrs.Tests.Coroutine; /// /// 的单元测试类。 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 8c3aa9f7..360fe97b 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -3,9 +3,10 @@ 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.Tests.Logging; -namespace GFramework.Core.Tests.Cqrs; +namespace GFramework.Cqrs.Tests.Cqrs; /// /// 验证 CQRS 处理器自动注册在顺序与容错层面的可观察行为。 diff --git a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs b/GFramework.Cqrs.Tests/CqrsTestRuntime.cs index f9bb143b..7676f143 100644 --- a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs +++ b/GFramework.Cqrs.Tests/CqrsTestRuntime.cs @@ -4,8 +4,9 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Tests; +namespace GFramework.Cqrs.Tests; /// /// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index cadb340b..257809f0 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -1,9 +1,9 @@ -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Mediator; /// /// Mediator高级特性专项测试 @@ -12,10 +12,6 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorAdvancedFeaturesTests { - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - [SetUp] public void SetUp() { @@ -44,6 +40,10 @@ public class MediatorAdvancedFeaturesTests _container = null; } + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + [Test] public async Task Request_With_Validation_Behavior_Should_Validate_Input() diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index 7d73a45e..e403735e 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,13 +1,13 @@ using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; -namespace GFramework.Core.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Mediator; /// /// Mediator与架构上下文集成测试 @@ -16,11 +16,6 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorArchitectureIntegrationTests { - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - [SetUp] public void SetUp() { @@ -54,6 +49,11 @@ public class MediatorArchitectureIntegrationTests _commandBus = null; } + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + [Test] public async Task Handler_Can_Access_Architecture_Context() { diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index cdaf9af6..b0b510d6 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -1,5 +1,4 @@ using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Events; using GFramework.Core.Architectures; using GFramework.Core.Command; @@ -8,23 +7,14 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; -using Unit = GFramework.Core.Abstractions.Cqrs.Unit; -namespace GFramework.Core.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Mediator; [TestFixture] public class MediatorComprehensiveTests { - private AsyncQueryExecutor? _asyncQueryBus; - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - private DefaultEnvironment? _environment; - private EventBus? _eventBus; - private QueryExecutor? _queryBus; - /// /// 测试初始化方法,在每个测试方法执行前运行。 /// 负责初始化日志工厂、依赖注入容器、自有 CQRS 处理器以及各种总线服务。 @@ -79,6 +69,15 @@ public class MediatorComprehensiveTests _environment = null; } + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + /// /// 测试SendRequestAsync方法在请求有效时返回结果 /// diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs index 1c2699ae..71d523eb 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记类型允许为带映射特性的导出集合生成批量注册代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs index 3b73af7b..4e56cd9b 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记场景根节点类型,Source Generator 会生成场景行为样板代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs index 56957fb8..0356c43c 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记 UI 页面类型,Source Generator 会生成页面行为样板代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs index dd809fc4..c4fe14a2 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 声明导出集合应当转发到哪个注册器成员及其方法。 diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index 98d57936..6805d2e7 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -1,10 +1,10 @@ -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Extensions; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Core.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Godot.Coroutine; From 932235e8ccc806822e5717f90d7bea0d015741b1 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:36:08 +0800 Subject: [PATCH 29/70] =?UTF-8?q?refactor(tests):=20=E6=9B=B4=E6=96=B0Cqrs?= =?UTF-8?q?CoroutineExtensionsTests=E4=B8=AD=E7=9A=84=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加GFramework.Core.Coroutine.Extensions命名空间引用 - 保持现有测试功能完整性 - 优化代码结构以匹配最新框架变更 --- GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index bd41c1e4..ccc6e27f 100644 --- a/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -14,6 +14,7 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Coroutine.Extensions; using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Cqrs.Tests.Coroutine; From e2001766cbb76acb5e2bf50eab02fdf58d43bab3 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:57:08 +0800 Subject: [PATCH 30/70] =?UTF-8?q?feat(arch):=20=E6=B7=BB=E5=8A=A0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8A=E4=B8=8B=E6=96=87=E5=AE=9E=E7=8E=B0=E5=8F=8A?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 ArchitectureContext 类,提供对系统、模型、工具等组件的访问 - 集成 CQRS runtime,支持命令、查询、事件的执行管理 - 添加服务缓存机制,优化容器解析性能 - 实现并发安全的 CQRS runtime 懒加载 - 提供同步和异步的请求处理方法 - 支持优先级排序的服务实例获取 - 添加完整的单元测试覆盖构造函数、查询、命令、事件等功能 - 配置测试项目依赖和全局引用 - 实现共享的 CQRS 测试运行时支持 --- .../Architectures/ArchitectureContextTests.cs | 68 +++++++++- GFramework.Core.Tests/CqrsTestRuntime.cs | 119 ------------------ .../GFramework.Core.Tests.csproj | 1 + GFramework.Core.Tests/GlobalUsings.cs | 1 + .../Architectures/ArchitectureContext.cs | 41 +++++- .../GFramework.Cqrs.Tests.csproj | 1 + GFramework.Cqrs.Tests/GlobalUsings.cs | 1 + .../Shared}/CqrsTestRuntime.cs | 30 ++--- 8 files changed, 118 insertions(+), 144 deletions(-) delete mode 100644 GFramework.Core.Tests/CqrsTestRuntime.cs rename {GFramework.Cqrs.Tests => tests/Shared}/CqrsTestRuntime.cs (76%) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 00684cb9..4b5da422 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -1,8 +1,10 @@ using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; +using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; @@ -14,6 +16,7 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -298,6 +301,69 @@ public class ArchitectureContextTests Assert.That(environment, Is.Not.Null); Assert.That(environment, Is.InstanceOf()); } + + /// + /// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。 + /// + [Test] + public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() + { + using var startGate = new ManualResetEventSlim(false); + using var allowResolutionToComplete = new ManualResetEventSlim(false); + var resolutionCallCount = 0; + var runtime = new Mock(MockBehavior.Strict); + var container = new Mock(MockBehavior.Strict); + + runtime.Setup(mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask(42)); + + container.Setup(mockContainer => mockContainer.Get()) + .Returns(() => + { + Interlocked.Increment(ref resolutionCallCount); + allowResolutionToComplete.Wait(); + return runtime.Object; + }); + + var context = new ArchitectureContext(container.Object); + var requests = Enumerable.Range(0, 16) + .Select(_ => Task.Run(async () => + { + startGate.Wait(); + return await context.SendRequestAsync(new TestCqrsRequest()); + })) + .ToArray(); + + startGate.Set(); + + Assert.That( + SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, TimeSpan.FromSeconds(1)), + Is.True, + "Expected at least one CQRS runtime resolution attempt."); + + // 留出一个短暂窗口,让并发首次访问都在 runtime 尚未发布前抵达同一初始化点。 + await Task.Delay(50); + allowResolutionToComplete.Set(); + + var responses = await Task.WhenAll(requests); + + Assert.That(responses, Has.All.EqualTo(42)); + Assert.That(resolutionCallCount, Is.EqualTo(1)); + container.Verify(mockContainer => mockContainer.Get(), Times.Once); + runtime.Verify( + mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Exactly(requests.Length)); + } + + private sealed class TestCqrsRequest : IRequest + { + } } #region Test Classes @@ -442,4 +508,4 @@ public class TestEventV2 public int Data { get; init; } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs deleted file mode 100644 index 3f8a7813..00000000 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; -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; - -namespace GFramework.Core.Tests; - -/// -/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 -/// -/// -/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, -/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 -/// -internal static class CqrsTestRuntime -{ - private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate CqrsHandlerRegistrar type."); - - private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType - .GetMethod( - "RegisterHandlers", - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static, - binder: null, - [ - typeof(IIocContainer), - typeof(IEnumerable), - typeof(ILogger) - ], - modifiers: null) - ?? throw new InvalidOperationException( - "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); - - private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsDispatcher", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate CqrsDispatcher type."); - - private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( - BindingFlags.Instance | - BindingFlags.Public | - BindingFlags.NonPublic, - binder: null, - [ - typeof(IIocContainer), - typeof(ILogger) - ], - modifiers: null) - ?? throw new InvalidOperationException( - "Failed to locate CqrsDispatcher constructor."); - - private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate DefaultCqrsHandlerRegistrar type."); - - private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = - DefaultCqrsHandlerRegistrarType.GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - binder: null, - [ - typeof(IIocContainer), - typeof(ILogger) - ], - modifiers: null) - ?? throw new InvalidOperationException( - "Failed to locate DefaultCqrsHandlerRegistrar constructor."); - - /// - /// 为裸测试容器补齐默认 CQRS runtime seam。 - /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, - /// 而无需完整启动服务模块管理器。 - /// - /// 目标测试容器。 - internal static void RegisterInfrastructure(MicrosoftDiContainer container) - { - ArgumentNullException.ThrowIfNull(container); - - var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); - var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); - var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); - var registrar = - (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); - - container.Register(runtime); - container.Register(registrar); - } - - /// - /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 - /// - /// 承载处理器映射的测试容器。 - /// 要扫描的程序集集合。 - internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) - { - ArgumentNullException.ThrowIfNull(container); - ArgumentNullException.ThrowIfNull(assemblies); - - RegisterInfrastructure(container); - - var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); - RegisterHandlersMethod.Invoke( - null, - [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]); - } -} diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index 97663fe0..c455f570 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index fe9b7de1..daaaf5b7 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -16,6 +16,7 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; +global using GFramework.Tests.Shared; global using NUnit.Framework; global using NUnit.Compatibility; global using GFramework.Core.Systems; diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index e52960bf..9b6d7dc2 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -17,19 +17,48 @@ namespace GFramework.Core.Architectures; /// /// 架构上下文类,提供对系统、模型、工具等组件的访问以及命令、查询、事件的执行管理 /// -public class ArchitectureContext(IIocContainer container) : IArchitectureContext +public class ArchitectureContext : IArchitectureContext { - private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly IIocContainer _container; + private readonly Lazy _cqrsRuntime; private readonly ConcurrentDictionary _serviceCache = new(); - private ICqrsRuntime? _cqrsRuntime; + + /// + /// 初始化新的架构上下文,并绑定其依赖容器。 + /// + /// + /// 当前架构使用的 IOC 容器。 + /// CQRS runtime 与其他框架服务会通过该容器延迟解析,以避免在上下文构造阶段强制拉起整条运行时链路。 + /// + /// + public ArchitectureContext(IIocContainer container) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _cqrsRuntime = new Lazy( + ResolveCqrsRuntime, + LazyThreadSafetyMode.ExecutionAndPublication); + } #region CQRS Integration /// - /// 获取 CQRS runtime seam(延迟初始化)。 + /// 获取 CQRS runtime seam。 /// - private ICqrsRuntime CqrsRuntime => _cqrsRuntime ??= - _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); + /// + /// 该实例会在首次访问时从容器解析,并通过 保证并发场景下只执行一次初始化, + /// 避免多个请求线程重复触发同一个 runtime 的容器解析。 + /// + private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value; + + /// + /// 从容器解析当前架构上下文依赖的 CQRS runtime。 + /// + /// 已注册的 CQRS runtime 实例。 + /// 容器中未注册 + private ICqrsRuntime ResolveCqrsRuntime() + { + return _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); + } /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj index 09cded18..6c05a59d 100644 --- a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs index d31630ed..9a04018e 100644 --- a/GFramework.Cqrs.Tests/GlobalUsings.cs +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -20,6 +20,7 @@ global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Threading; global using System.Threading.Tasks; +global using GFramework.Tests.Shared; global using Microsoft.Extensions.DependencyInjection; global using Moq; global using NUnit.Compatibility; diff --git a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs b/tests/Shared/CqrsTestRuntime.cs similarity index 76% rename from GFramework.Cqrs.Tests/CqrsTestRuntime.cs rename to tests/Shared/CqrsTestRuntime.cs index 7676f143..a234ff90 100644 --- a/GFramework.Cqrs.Tests/CqrsTestRuntime.cs +++ b/tests/Shared/CqrsTestRuntime.cs @@ -6,23 +6,21 @@ using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Cqrs.Tests; +namespace GFramework.Tests.Shared; /// /// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 /// /// -/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, -/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 +/// 该文件以共享源码的方式同时编译进多个测试项目,确保反射绑定签名、默认 runtime 接线和注册入口行为始终保持一致, +/// 避免测试副本在独立演化后产生隐藏分歧。 /// internal static class CqrsTestRuntime { private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate CqrsHandlerRegistrar type."); + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)!; private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType .GetMethod( @@ -40,11 +38,9 @@ internal static class CqrsTestRuntime "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsDispatcher", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate CqrsDispatcher type."); + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsDispatcher", + throwOnError: true)!; private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( BindingFlags.Instance | @@ -60,11 +56,9 @@ internal static class CqrsTestRuntime "Failed to locate CqrsDispatcher constructor."); private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate DefaultCqrsHandlerRegistrar type."); + .GetType( + "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", + throwOnError: true)!; private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = DefaultCqrsHandlerRegistrarType.GetConstructor( From f9cc1237a3c8e2ed0db03dcb63fe0e4532ef955c Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:22:09 +0800 Subject: [PATCH 31/70] =?UTF-8?q?chore(project):=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 GFramework.Core.Tests 和 GFramework.Cqrs.Tests 测试项目 - 配置测试项目的全局 using 语句和依赖引用 - 添加主项目 GFramework 的元包配置文件 - 生成解决方案文件并配置所有项目引用关系 - 设置多目标框架支持 net8.0、net9.0 和 net10.0 - 配置包发布设置和仓库信息 --- .../GFramework.Core.Tests.csproj | 2 +- GFramework.Core.Tests/GlobalUsings.cs | 2 +- .../GFramework.Cqrs.Tests.csproj | 2 +- GFramework.Cqrs.Tests/GlobalUsings.cs | 2 +- .../CqrsTestRuntime.cs | 22 ++++++++++++------- .../GFramework.Tests.Common.csproj | 17 ++++++++++++++ GFramework.csproj | 3 +++ GFramework.sln | 14 ++++++++++++ 8 files changed, 52 insertions(+), 12 deletions(-) rename {tests/Shared => GFramework.Tests.Common}/CqrsTestRuntime.cs (90%) create mode 100644 GFramework.Tests.Common/GFramework.Tests.Common.csproj diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index c455f570..391b2924 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -18,7 +18,7 @@ - + diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index daaaf5b7..96b8bb1a 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -16,7 +16,7 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; -global using GFramework.Tests.Shared; +global using GFramework.Tests.Common; global using NUnit.Framework; global using NUnit.Compatibility; global using GFramework.Core.Systems; diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj index 6c05a59d..796883e4 100644 --- a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs index 9a04018e..c47473b0 100644 --- a/GFramework.Cqrs.Tests/GlobalUsings.cs +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -20,7 +20,7 @@ global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Threading; global using System.Threading.Tasks; -global using GFramework.Tests.Shared; +global using GFramework.Tests.Common; global using Microsoft.Extensions.DependencyInjection; global using Moq; global using NUnit.Compatibility; diff --git a/tests/Shared/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs similarity index 90% rename from tests/Shared/CqrsTestRuntime.cs rename to GFramework.Tests.Common/CqrsTestRuntime.cs index a234ff90..50890658 100644 --- a/tests/Shared/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; @@ -6,16 +10,16 @@ using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Tests.Shared; +namespace GFramework.Tests.Common; /// /// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 /// /// -/// 该文件以共享源码的方式同时编译进多个测试项目,确保反射绑定签名、默认 runtime 接线和注册入口行为始终保持一致, -/// 避免测试副本在独立演化后产生隐藏分歧。 +/// 该测试基础设施位于独立模块中,避免多个测试项目复制同一份反射绑定与默认 runtime 接线逻辑。 +/// 测试应通过该入口驱动注册流程,而不是各自维护一份实现细节副本。 /// -internal static class CqrsTestRuntime +public static class CqrsTestRuntime { private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly .GetType( @@ -74,11 +78,13 @@ internal static class CqrsTestRuntime /// /// 为裸测试容器补齐默认 CQRS runtime seam。 - /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, - /// 而无需完整启动服务模块管理器。 /// /// 目标测试容器。 - internal static void RegisterInfrastructure(MicrosoftDiContainer container) + /// + /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, + /// 而无需完整启动服务模块管理器。 + /// + public static void RegisterInfrastructure(MicrosoftDiContainer container) { ArgumentNullException.ThrowIfNull(container); @@ -97,7 +103,7 @@ internal static class CqrsTestRuntime /// /// 承载处理器映射的测试容器。 /// 要扫描的程序集集合。 - internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) + public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) { ArgumentNullException.ThrowIfNull(container); ArgumentNullException.ThrowIfNull(assemblies); diff --git a/GFramework.Tests.Common/GFramework.Tests.Common.csproj b/GFramework.Tests.Common/GFramework.Tests.Common.csproj new file mode 100644 index 00000000..51f8fba9 --- /dev/null +++ b/GFramework.Tests.Common/GFramework.Tests.Common.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + disable + enable + true + false + + + + + + + + + diff --git a/GFramework.csproj b/GFramework.csproj index 679cbf64..b9c6c7a4 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -66,6 +66,7 @@ + @@ -110,6 +111,7 @@ + @@ -140,6 +142,7 @@ + diff --git a/GFramework.sln b/GFramework.sln index 088d63d3..bc64ce21 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -44,6 +44,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramewo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Tests", "GFramework.Cqrs.Tests\GFramework.Cqrs.Tests.csproj", "{29037A55-9A89-425C-AB33-D44872B2E601}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Tests.Common", "GFramework.Tests.Common\GFramework.Tests.Common.csproj", "{1100EE3E-A12D-4DE5-ABA8-591D3126570B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -318,6 +320,18 @@ Global {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.Build.0 = Release|Any CPU {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.ActiveCfg = Release|Any CPU {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x64.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x86.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|Any CPU.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x64.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x64.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x86.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 81897ce2aca83b5ed2b6684283118bc589415911 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:00:49 +0800 Subject: [PATCH 32/70] =?UTF-8?q?docs(source-generators):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=BA=90=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AutoRegisterExportedCollections 生成器文档 - 新增 AutoScene 生成器文档 - 新增 AutoUiPage 生成器文档 - 新增完整的源码生成器索引文档 - 详细介绍各生成器的使用方法和参数说明 - 提供生成代码示例和诊断信息说明 - 包含性能优势和使用示例章节 --- .../ArchitectureServicesTests.cs | 30 ++++++++++++ .../MediatorCompatibilityDeprecationTests.cs | 8 ++-- .../Extensions/MediatorCoroutineExtensions.cs | 2 +- .../Cqrs/Internal/CqrsDispatcher.cs | 6 +++ .../ContextAwareMediatorCommandExtensions.cs | 2 +- .../ContextAwareMediatorExtensions.cs | 2 +- .../ContextAwareMediatorQueryExtensions.cs | 2 +- .../Behavior/AutoSceneGeneratorTests.cs | 20 ++++---- .../Behavior/AutoUiPageGeneratorTests.cs | 12 ++--- ...gisterExportedCollectionsGeneratorTests.cs | 48 +++++++++---------- .../Behavior/AutoSceneGenerator.cs | 3 +- .../Behavior/AutoUiPageGenerator.cs | 2 +- ...utoRegisterExportedCollectionsGenerator.cs | 4 +- ...register-exported-collections-generator.md | 3 +- .../source-generators/auto-scene-generator.md | 3 +- .../auto-ui-page-generator.md | 5 +- docs/zh-CN/source-generators/index.md | 6 +-- 17 files changed, 99 insertions(+), 59 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs index 57dc19a5..52c2ecbd 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs @@ -359,6 +359,14 @@ public class TestArchitectureContextV3 : IArchitectureContext throw new NotImplementedException(); } + /// + /// 测试桩:异步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendCommandAsync( GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) @@ -366,11 +374,26 @@ public class TestArchitectureContextV3 : IArchitectureContext throw new NotImplementedException(); } + /// + /// 测试桩:同步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 命令响应。 + /// 该测试桩未实现此成员。 public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 取消令牌。 + /// 查询结果任务。 + /// 该测试桩未实现此成员。 public ValueTask SendQueryAsync( GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) @@ -378,6 +401,13 @@ public class TestArchitectureContextV3 : IArchitectureContext throw new NotImplementedException(); } + /// + /// 测试桩:同步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 查询结果。 + /// 该测试桩未实现此成员。 public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs index 2e3bdbca..b25806f0 100644 --- a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -35,16 +35,16 @@ public class MediatorCompatibilityDeprecationTests { AssertLegacyType( typeof(ContextAwareMediatorExtensions), - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead."); + "Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorCommandExtensions), - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead."); + "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorQueryExtensions), - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead."); + "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead."); AssertLegacyType( typeof(MediatorCoroutineExtensions), - "Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead."); + "Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead."); } /// diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index 70e3c29f..5086e392 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -25,7 +25,7 @@ namespace GFramework.Core.Coroutine.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")] public static class MediatorCoroutineExtensions { /// diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs index 4e2c739d..0cae34c4 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs @@ -17,14 +17,20 @@ internal sealed class CqrsDispatcher( IIocContainer container, ILogger logger) : ICqrsRuntime { + // 进程级缓存:按请求/响应类型缓存直接处理器调用委托,避免热路径重复反射。 + // 线程安全依赖 ConcurrentDictionary;缓存与进程同寿命,默认假设请求类型集合有限且稳定。 private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> RequestInvokers = new(); + // 进程级缓存:缓存带 pipeline 的请求调用委托,减少每次分发时的反射与表达式重建开销。 + // 若后续引入动态生成请求类型,需要重新评估该缓存的增长边界。 private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> RequestPipelineInvokers = new(); + // 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。 private static readonly ConcurrentDictionary NotificationInvokers = new(); + // 进程级缓存:缓存流式请求调用委托,避免每次创建流时重复解析反射签名。 private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new(); diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 41d9df27..4ab0692b 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -11,7 +11,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorCommandExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index b4d607bc..f2294930 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -11,7 +11,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index 49b57445..61b02cb9 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -11,7 +11,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorQueryExtensions { /// diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index b50b0d73..2c6dc972 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -11,10 +11,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -88,10 +88,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -137,10 +137,10 @@ public class AutoSceneGeneratorTests const string source = """ #nullable enable using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -225,10 +225,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -279,10 +279,10 @@ public class AutoSceneGeneratorTests const string source = """ using System; using GFramework.Game.Abstractions.Scene; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs index 8e7d1115..2b8aca67 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs @@ -11,10 +11,10 @@ public class AutoUiPageGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute @@ -100,10 +100,10 @@ public class AutoUiPageGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute @@ -183,10 +183,10 @@ public class AutoUiPageGeneratorTests const string source = """ #nullable enable using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 3a67d3c5..befdcf59 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -13,9 +13,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -86,9 +86,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -141,9 +141,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -207,9 +207,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -284,9 +284,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -344,9 +344,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -414,9 +414,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -482,9 +482,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -549,9 +549,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -604,9 +604,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -659,9 +659,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -715,9 +715,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs index f2920be4..ee389f0e 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs @@ -18,7 +18,8 @@ namespace GFramework.Godot.SourceGenerators.Behavior; public sealed class AutoSceneGenerator : IIncrementalGenerator { private const string AutoSceneAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoSceneAttribute"; + private static readonly string[] GeneratedMemberNames = [ "SceneKeyStr", diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs index 1e0fe7f6..ca193956 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs @@ -12,7 +12,7 @@ namespace GFramework.Godot.SourceGenerators.Behavior; public sealed class AutoUiPageGenerator : IIncrementalGenerator { private const string AutoUiPageAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoUiPageAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoUiPageAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index 12589767..93219dcb 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -18,10 +18,10 @@ namespace GFramework.Godot.SourceGenerators.Registration; public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator { private const string AutoRegisterExportedCollectionsAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoRegisterExportedCollectionsAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoRegisterExportedCollectionsAttribute"; private const string RegisterExportedCollectionAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.RegisterExportedCollectionAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.RegisterExportedCollectionAttribute"; private const string GeneratedMethodName = "__RegisterExportedCollections_Generated"; diff --git a/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md b/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md index 2d73f531..8d1ca366 100644 --- a/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md +++ b/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md @@ -13,12 +13,13 @@ `AutoRegisterExportedCollections` 会把这类样板收敛成声明式配置。 它特别适合 `GameEntryPoint`、资源根节点、配置引导节点这类“导出即注册”的场景。 +相关特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp using System.Collections.Generic; -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; public interface IKeyValue diff --git a/docs/zh-CN/source-generators/auto-scene-generator.md b/docs/zh-CN/source-generators/auto-scene-generator.md index 1a2ff881..92927daa 100644 --- a/docs/zh-CN/source-generators/auto-scene-generator.md +++ b/docs/zh-CN/source-generators/auto-scene-generator.md @@ -12,11 +12,12 @@ - `GetScene()` 包装方法 `AutoScene` 会在编译期生成这些固定样板。 +该特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; diff --git a/docs/zh-CN/source-generators/auto-ui-page-generator.md b/docs/zh-CN/source-generators/auto-ui-page-generator.md index b455eab8..1c1ecd48 100644 --- a/docs/zh-CN/source-generators/auto-ui-page-generator.md +++ b/docs/zh-CN/source-generators/auto-ui-page-generator.md @@ -13,11 +13,12 @@ - `GetPage()` 工厂包装 `AutoUiPage` 会把这部分样板迁移到编译期生成。 +该特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -123,7 +124,7 @@ partial class MainMenu ## 组合示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using GFramework.SourceGenerators.Abstractions.Rule; using Godot; diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md index 8fd91e66..ea62aad0 100644 --- a/docs/zh-CN/source-generators/index.md +++ b/docs/zh-CN/source-generators/index.md @@ -610,7 +610,7 @@ AutoUiPage 生成器为 Godot 页面节点自动生成 `UiKeyStr`、缓存的 `I ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -635,7 +635,7 @@ AutoScene 生成器为场景根节点自动生成 `SceneKeyStr`、缓存的 `ISc ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -660,7 +660,7 @@ AutoRegisterExportedCollections 生成器为 Godot 导出集合自动生成批 ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; [AutoRegisterExportedCollections] From 82e6332a9b7a8bc7b62bea2728ccc44ad9bd3d26 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:47:54 +0800 Subject: [PATCH 33/70] =?UTF-8?q?test(core):=20=E6=B7=BB=E5=8A=A0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8A=E4=B8=8B=E6=96=87=E5=92=8C=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E5=AE=B9=E5=99=A8=E7=9A=84=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 ArchitectureContext 类的全面单元测试,覆盖构造函数、查询命令事件发送等功能 - 添加 MicrosoftDiContainer 依赖注入容器的完整测试,包括注册、获取、冻结等操作 - 创建 CqrsTestRuntime 测试基础设施,提供对 CQRS 处理器注册的受控访问 - 测试并发场景下的线程安全性,验证多线程环境下容器操作的正确性 - 实现优先级排序功能测试,确保服务按优先级正确排序和注册 - 添加各种边界条件测试,包括空参数异常处理和重复注册异常检测 --- .../Architectures/ArchitectureContextTests.cs | 29 +++++++++------- .../Ioc/MicrosoftDiContainerTests.cs | 21 ++++++++++-- GFramework.Tests.Common/CqrsTestRuntime.cs | 34 ++++++++++++++----- 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 4b5da422..63d7ffae 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -16,7 +16,6 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; -using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -45,6 +44,15 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureContextTests { + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + [SetUp] public void SetUp() { @@ -76,14 +84,6 @@ public class ArchitectureContextTests _context = new ArchitectureContext(_container); } - private ArchitectureContext? _context; - private MicrosoftDiContainer? _container; - private EventBus? _eventBus; - private CommandExecutor? _commandBus; - private QueryExecutor? _queryBus; - private AsyncQueryExecutor? _asyncQueryBus; - private DefaultEnvironment? _environment; - /// /// 测试构造函数在所有参数都有效时不应抛出异常 /// @@ -308,8 +308,10 @@ public class ArchitectureContextTests [Test] public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() { + const int workerCount = 16; 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); @@ -329,14 +331,19 @@ public class ArchitectureContextTests }); var context = new ArchitectureContext(container.Object); - var requests = Enumerable.Range(0, 16) + var requests = Enumerable.Range(0, workerCount) .Select(_ => Task.Run(async () => { + workersReady.Signal(); startGate.Wait(); return await context.SendRequestAsync(new TestCqrsRequest()); })) .ToArray(); + Assert.That( + workersReady.Wait(TimeSpan.FromSeconds(1)), + Is.True, + "Expected all workers to be ready before releasing start gate."); startGate.Set(); Assert.That( @@ -344,8 +351,6 @@ public class ArchitectureContextTests Is.True, "Expected at least one CQRS runtime resolution attempt."); - // 留出一个短暂窗口,让并发首次访问都在 runtime 尚未发布前抵达同一初始化点。 - await Task.Delay(50); allowResolutionToComplete.Set(); var responses = await Task.WhenAll(requests); diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 3faedecb..275101d9 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,10 +1,10 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; -using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Ioc; @@ -14,6 +14,8 @@ namespace GFramework.Core.Tests.Ioc; [TestFixture] public class MicrosoftDiContainerTests { + private MicrosoftDiContainer _container = null!; + /// /// 在每个测试方法执行前进行设置 /// @@ -33,8 +35,6 @@ public class MicrosoftDiContainerTests CqrsTestRuntime.RegisterInfrastructure(_container); } - private MicrosoftDiContainer _container = null!; - /// /// 测试注册单例实例的功能 /// @@ -151,6 +151,21 @@ public class MicrosoftDiContainerTests Assert.That(result, Is.SameAs(instance)); } + /// + /// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。 + /// + [Test] + public void RegisterHandlers_Should_Not_Duplicate_Cqrs_Infrastructure_When_It_Is_Already_Registered() + { + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + + CqrsTestRuntime.RegisterHandlers(_container); + + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + } + /// /// 测试当没有实例时获取应返回 null 的功能 /// diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index 50890658..57d4effd 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; @@ -80,22 +79,31 @@ public static class CqrsTestRuntime /// 为裸测试容器补齐默认 CQRS runtime seam。 /// /// 目标测试容器。 + /// + /// 反射调用底层 CQRS runtime 或注册器构造函数失败时抛出。 /// /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, /// 而无需完整启动服务模块管理器。 + /// 该方法按服务类型执行幂等注册,只会补齐当前容器中尚未接线的 CQRS 基础设施。 /// public static void RegisterInfrastructure(MicrosoftDiContainer container) { ArgumentNullException.ThrowIfNull(container); - var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); - var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); - var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); - var registrar = - (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + if (container.Get() is null) + { + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(CqrsDispatcherType.Name); + var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + container.Register(runtime); + } - container.Register(runtime); - container.Register(registrar); + if (container.Get() is null) + { + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(DefaultCqrsHandlerRegistrarType.Name); + var registrar = + (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + container.Register(registrar); + } } /// @@ -103,6 +111,14 @@ public static class CqrsTestRuntime /// /// 承载处理器映射的测试容器。 /// 要扫描的程序集集合。 + /// + /// 。 + /// + /// 反射调用底层 CQRS 处理器注册入口失败时抛出。 + /// + /// 该入口会自动调用 ,因此测试通常无需预先手动接线 CQRS 基础设施。 + /// 程序集去重与空元素过滤由生产注册入口统一处理,避免测试辅助层复制相同筛选逻辑。 + /// public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) { ArgumentNullException.ThrowIfNull(container); @@ -113,6 +129,6 @@ public static class CqrsTestRuntime var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); RegisterHandlersMethod.Invoke( null, - [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]); + [container, assemblies, logger]); } } From 2425d280979258bdd5329819c4af7e94b0599bff Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:49:52 +0800 Subject: [PATCH 34/70] =?UTF-8?q?refactor(tests):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?Cqrs=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ArchitectureContextTests.cs中添加GFramework.Cqrs.Abstractions.Cqrs命名空间引用 - 在MicrosoftDiContainerTests.cs中添加GFramework.Cqrs.Abstractions.Cqrs命名空间引用 - 为测试文件提供必要的Cqrs相关依赖注入支持 --- .../Architectures/ArchitectureContextTests.cs | 19 ++++++++++--------- .../Ioc/MicrosoftDiContainerTests.cs | 5 +++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 63d7ffae..dd524414 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -16,6 +16,7 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -44,15 +45,6 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureContextTests { - private AsyncQueryExecutor? _asyncQueryBus; - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - private DefaultEnvironment? _environment; - private EventBus? _eventBus; - private QueryExecutor? _queryBus; - [SetUp] public void SetUp() { @@ -84,6 +76,15 @@ public class ArchitectureContextTests _context = new ArchitectureContext(_container); } + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + /// /// 测试构造函数在所有参数都有效时不应抛出异常 /// diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 275101d9..0e03059e 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -5,6 +5,7 @@ using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Ioc; @@ -14,8 +15,6 @@ namespace GFramework.Core.Tests.Ioc; [TestFixture] public class MicrosoftDiContainerTests { - private MicrosoftDiContainer _container = null!; - /// /// 在每个测试方法执行前进行设置 /// @@ -35,6 +34,8 @@ public class MicrosoftDiContainerTests CqrsTestRuntime.RegisterInfrastructure(_container); } + private MicrosoftDiContainer _container = null!; + /// /// 测试注册单例实例的功能 /// From a068a5e707291c118a0b8bd1eb046d0bee4ed030 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:52:53 +0800 Subject: [PATCH 35/70] =?UTF-8?q?test(arch):=20=E4=BC=98=E5=8C=96=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8A=E4=B8=8B=E6=96=87=E5=B9=B6=E5=8F=91=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=9A=84=E8=B6=85=E6=97=B6=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将工作线程数量从16调整为8 - 添加工作线程启动超时设置为5秒 - 添加首次解析超时设置为5秒 - 使用可配置超时替代硬编码的1秒等待时间 - 提高测试稳定性和可读性 --- .../Architectures/ArchitectureContextTests.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index dd524414..09c3f4d1 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -16,7 +16,6 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; -using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -45,6 +44,15 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureContextTests { + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + [SetUp] public void SetUp() { @@ -76,15 +84,6 @@ public class ArchitectureContextTests _context = new ArchitectureContext(_container); } - private AsyncQueryExecutor? _asyncQueryBus; - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - private DefaultEnvironment? _environment; - private EventBus? _eventBus; - private QueryExecutor? _queryBus; - /// /// 测试构造函数在所有参数都有效时不应抛出异常 /// @@ -309,7 +308,9 @@ public class ArchitectureContextTests [Test] public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() { - const int workerCount = 16; + 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); @@ -342,13 +343,13 @@ public class ArchitectureContextTests .ToArray(); Assert.That( - workersReady.Wait(TimeSpan.FromSeconds(1)), + 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, TimeSpan.FromSeconds(1)), + SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), Is.True, "Expected at least one CQRS runtime resolution attempt."); From fe73d13991f5fcc222ddfb1c39aac43a39aac60e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:55:41 +0800 Subject: [PATCH 36/70] =?UTF-8?q?chore(tests):=20=E6=B7=BB=E5=8A=A0Cqrs?= =?UTF-8?q?=E6=8A=BD=E8=B1=A1=E4=BE=9D=E8=B5=96=E5=88=B0=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ArchitectureContextTests中添加GFramework.Cqrs.Abstractions.Cqrs命名空间引用 --- .../Architectures/ArchitectureContextTests.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 09c3f4d1..87978cd2 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -16,6 +16,7 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -44,15 +45,6 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureContextTests { - private AsyncQueryExecutor? _asyncQueryBus; - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - private DefaultEnvironment? _environment; - private EventBus? _eventBus; - private QueryExecutor? _queryBus; - [SetUp] public void SetUp() { @@ -84,6 +76,15 @@ public class ArchitectureContextTests _context = new ArchitectureContext(_container); } + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + /// /// 测试构造函数在所有参数都有效时不应抛出异常 /// From d881bd5ad1db58f344ad61bbbaa704bb99233fe6 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:02:53 +0800 Subject: [PATCH 37/70] =?UTF-8?q?refactor(cqrs):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AE=9A=E4=B9=89=E5=B9=B6=E5=88=86=E7=A6=BB?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E5=91=BD=E4=BB=A4=E5=92=8C=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 ICommand 和 ICommand 的空实现体 - 移除 IQuery 和 IStreamQuery 的空实现体 - 移除 INotification、IRequest、IStreamRequest 的空实现体 - 将 IStreamCommand 分离到独立文件中 - 将 IStreamQuery 分离到独立文件中 - 保持所有接口的核心功能不变,仅简化语法结构 --- .../Cqrs/Command/ICommand.cs | 16 ++------------- .../Cqrs/Command/IStreamCommand.cs | 20 +++++++++++++++++++ .../Cqrs/INotification.cs | 4 +--- GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs | 4 +--- .../Cqrs/IStreamRequest.cs | 4 +--- .../Cqrs/Query/IQuery.cs | 12 +---------- .../Cqrs/Query/IStreamQuery.cs | 20 +++++++++++++++++++ 7 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs create mode 100644 GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs index 6eaa1869..9c62f8f5 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs @@ -5,21 +5,9 @@ namespace GFramework.Cqrs.Abstractions.Cqrs.Command; /// 命令通常用于修改系统状态。 /// /// 命令响应类型。 -public interface ICommand : IRequest -{ -} +public interface ICommand : IRequest; /// /// 表示一个无显式返回值的 CQRS 命令。 /// -public interface ICommand : ICommand -{ -} - -/// -/// 表示一个流式 CQRS 命令。 -/// -/// 流式响应元素类型。 -public interface IStreamCommand : IStreamRequest -{ -} +public interface ICommand : ICommand; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs new file mode 100644 index 00000000..51323d19 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; + +/// +/// 表示一个流式 CQRS 命令。 +/// +/// 流式响应元素类型。 +public interface IStreamCommand : IStreamRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs index 727f9c07..4a2dbb68 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs @@ -4,6 +4,4 @@ namespace GFramework.Cqrs.Abstractions.Cqrs; /// 表示一个一对多发布的通知消息。 /// 通知不要求返回值,允许被零个或多个处理器消费。 /// -public interface INotification -{ -} +public interface INotification; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs index 78627e4b..dd6abb62 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs @@ -5,6 +5,4 @@ namespace GFramework.Cqrs.Abstractions.Cqrs; /// 该接口是命令、查询以及其他请求语义的统一基接口。 /// /// 请求响应类型。 -public interface IRequest -{ -} +public interface IRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs index 37a211d4..5464459b 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs @@ -5,6 +5,4 @@ namespace GFramework.Cqrs.Abstractions.Cqrs; /// 请求处理器可以逐步产生响应序列,而不是一次性返回完整结果。 /// /// 流式响应元素类型。 -public interface IStreamRequest -{ -} +public interface IStreamRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs index 9592a9bd..edf5e1a2 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs @@ -5,14 +5,4 @@ namespace GFramework.Cqrs.Abstractions.Cqrs.Query; /// 查询用于读取数据,不应产生副作用。 /// /// 查询响应类型。 -public interface IQuery : IRequest -{ -} - -/// -/// 表示一个流式 CQRS 查询。 -/// -/// 流式响应元素类型。 -public interface IStreamQuery : IStreamRequest -{ -} +public interface IQuery : IRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs new file mode 100644 index 00000000..150fb32c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; + +/// +/// 表示一个流式 CQRS 查询。 +/// +/// 流式响应元素类型。 +public interface IStreamQuery : IStreamRequest; From a80ff596317598aff285662ec19141028ef918c4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:42:08 +0800 Subject: [PATCH 38/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A8=A1=E5=9D=97=E5=92=8C=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增ContextAwareMediatorCommandExtensions提供命令扩展方法的兼容性别名 - 新增ContextAwareMediatorExtensions提供CQRS统一接口扩展方法的兼容性别名 - 新增ContextAwareMediatorQueryExtensions提供查询扩展方法的兼容性别名 - 添加CqrsRuntimeModule用于注册CQRS运行时和处理器注册器到依赖注入容器 - 更新IArchitectureContext接口添加新版CQRS请求、命令、查询和通知的统一入口 - 添加架构上下文的CQRS处理器注册相关单元测试 - 配置项目文件以支持多目标框架和包引用管理 --- .../Architectures/IArchitectureContext.cs | 4 +- ...ArchitectureAdditionalCqrsHandlersTests.cs | 2 +- .../ContextAwareMediatorCommandExtensions.cs | 1 + .../ContextAwareMediatorExtensions.cs | 1 + .../ContextAwareMediatorQueryExtensions.cs | 1 + GFramework.Core/GFramework.Core.csproj | 1 + .../Services/Modules/CqrsRuntimeModule.cs | 11 ++-- .../Cqrs/CqrsHandlerRegistrarTests.cs | 1 - .../Command/CommandBase.cs | 2 +- .../CqrsHandlerRegistryAttribute.cs | 2 +- GFramework.Cqrs/CqrsRuntimeFactory.cs | 45 ++++++++++++++++ .../ContextAwareCqrsCommandExtensions.cs | 2 +- .../Extensions/ContextAwareCqrsExtensions.cs | 2 +- .../ContextAwareCqrsQueryExtensions.cs | 2 +- GFramework.Cqrs/GFramework.Cqrs.csproj | 1 + GFramework.Cqrs/GlobalUsings.cs | 6 +++ .../ICqrsHandlerRegistry.cs | 2 +- .../Internal/CqrsDispatcher.cs | 2 +- .../Internal/CqrsHandlerRegistrar.cs | 3 +- .../Internal/DefaultCqrsHandlerRegistrar.cs | 2 +- .../Notification/NotificationBase.cs | 2 +- .../Query/QueryBase.cs | 2 +- .../Request/RequestBase.cs | 2 +- .../ContextAwareCoroutineExtensions.cs | 2 +- .../Constants/PathContests.cs | 12 ++++- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 30 ++++++----- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 20 +++++--- GFramework.Tests.Common/CqrsTestRuntime.cs | 51 ++++--------------- .../GFramework.Tests.Common.csproj | 1 + 29 files changed, 129 insertions(+), 86 deletions(-) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Command/CommandBase.cs (97%) rename {GFramework.Core.Abstractions/Cqrs => GFramework.Cqrs}/CqrsHandlerRegistryAttribute.cs (94%) create mode 100644 GFramework.Cqrs/CqrsRuntimeFactory.cs rename {GFramework.Core => GFramework.Cqrs}/Extensions/ContextAwareCqrsCommandExtensions.cs (98%) rename {GFramework.Core => GFramework.Cqrs}/Extensions/ContextAwareCqrsExtensions.cs (99%) rename {GFramework.Core => GFramework.Cqrs}/Extensions/ContextAwareCqrsQueryExtensions.cs (98%) create mode 100644 GFramework.Cqrs/GlobalUsings.cs rename {GFramework.Core.Abstractions/Cqrs => GFramework.Cqrs}/ICqrsHandlerRegistry.cs (95%) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Internal/CqrsDispatcher.cs (99%) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Internal/CqrsHandlerRegistrar.cs (99%) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Internal/DefaultCqrsHandlerRegistrar.cs (96%) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Notification/NotificationBase.cs (96%) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Query/QueryBase.cs (97%) rename {GFramework.Core/Cqrs => GFramework.Cqrs}/Request/RequestBase.cs (97%) diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index 0eb6a43f..c7b62e2f 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -15,7 +15,7 @@ namespace GFramework.Core.Abstractions.Architectures; /// /// /// 旧的 GFramework.Core.Abstractions.CommandGFramework.Core.Abstractions.Query 契约会继续通过原有 Command/Query Executor 路径执行,以保证存量代码兼容。 -/// 新的 GFramework.Core.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 +/// 新的 GFramework.Cqrs.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 /// 新功能优先使用 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。 /// public interface IArchitectureContext @@ -175,7 +175,7 @@ public interface IArchitectureContext /// 要发送的 CQRS 查询。 /// 查询结果。 /// - /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse>。 + /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>。 /// TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query); diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index 67255b41..ddec08c8 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -1,8 +1,8 @@ using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Logging; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 4ab0692b..d001cb71 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Extensions; namespace GFramework.Core.Extensions; diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index f2294930..c7aec1b6 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Extensions; namespace GFramework.Core.Extensions; diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index 61b02cb9..4eb1b2c7 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Extensions; namespace GFramework.Core.Extensions; diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index f3e41eab..2535d4e9 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 5ca7a909..9e7ad307 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -1,8 +1,8 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Cqrs.Internal; using GFramework.Core.Logging; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Services.Modules; @@ -37,11 +37,12 @@ public sealed class CqrsRuntimeModule : IServiceModule { ArgumentNullException.ThrowIfNull(container); - var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsDispatcher)); - var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(DefaultCqrsHandlerRegistrar)); + var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); - container.Register(new CqrsDispatcher(container, dispatcherLogger)); - container.Register(new DefaultCqrsHandlerRegistrar(container, registrarLogger)); + container.Register(CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger)); + container.Register( + CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger)); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 360fe97b..3100ce84 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -1,4 +1,3 @@ -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; diff --git a/GFramework.Core/Cqrs/Command/CommandBase.cs b/GFramework.Cqrs/Command/CommandBase.cs similarity index 97% rename from GFramework.Core/Cqrs/Command/CommandBase.cs rename to GFramework.Cqrs/Command/CommandBase.cs index d8232608..57703c47 100644 --- a/GFramework.Core/Cqrs/Command/CommandBase.cs +++ b/GFramework.Cqrs/Command/CommandBase.cs @@ -13,7 +13,7 @@ using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Command; /// /// 表示一个基础命令类,用于处理带有输入和响应的命令模式实现。 diff --git a/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs b/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs similarity index 94% rename from GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs rename to GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs index e073fe5c..a64875f3 100644 --- a/GFramework.Core.Abstractions/Cqrs/CqrsHandlerRegistryAttribute.cs +++ b/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs; /// /// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。 diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs new file mode 100644 index 00000000..45dbb07a --- /dev/null +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -0,0 +1,45 @@ +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Internal; + +namespace GFramework.Cqrs; + +/// +/// 提供 CQRS runtime 默认实现的跨程序集创建入口。 +/// +/// +/// 需要在不暴露内部实现细节的前提下接入默认 CQRS runtime, +/// 因此通过该工厂返回抽象接口,而不是直接公开内部 dispatcher / registrar 类型。 +/// +public static class CqrsRuntimeFactory +{ + /// + /// 创建默认 CQRS runtime 分发器。 + /// + /// 目标依赖注入容器。 + /// 用于 runtime 诊断的日志器。 + /// 默认 CQRS runtime。 + public static ICqrsRuntime CreateRuntime(IIocContainer container, ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(logger); + + return new CqrsDispatcher(container, logger); + } + + /// + /// 创建默认 CQRS 处理器注册器。 + /// + /// 目标依赖注入容器。 + /// 用于注册阶段诊断的日志器。 + /// 默认 CQRS handler registrar。 + public static ICqrsHandlerRegistrar CreateHandlerRegistrar(IIocContainer container, ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(logger); + + return new DefaultCqrsHandlerRegistrar(container, logger); + } +} diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs similarity index 98% rename from GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs rename to GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs index f99fae25..b3c2bde8 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Extensions; +namespace GFramework.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs similarity index 99% rename from GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs rename to GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs index 6db156f5..ef5eb247 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Extensions; +namespace GFramework.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 统一扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs similarity index 98% rename from GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs rename to GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs index 40ec0f7d..21c1e952 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Extensions; +namespace GFramework.Cqrs.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 diff --git a/GFramework.Cqrs/GFramework.Cqrs.csproj b/GFramework.Cqrs/GFramework.Cqrs.csproj index 9f002283..86323d6e 100644 --- a/GFramework.Cqrs/GFramework.Cqrs.csproj +++ b/GFramework.Cqrs/GFramework.Cqrs.csproj @@ -11,6 +11,7 @@ + diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs new file mode 100644 index 00000000..7f8c14d6 --- /dev/null +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs b/GFramework.Cqrs/ICqrsHandlerRegistry.cs similarity index 95% rename from GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs rename to GFramework.Cqrs/ICqrsHandlerRegistry.cs index 91af6be7..db3775de 100644 --- a/GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs +++ b/GFramework.Cqrs/ICqrsHandlerRegistry.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Logging; -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs; /// /// 定义由源码生成器产出的 CQRS 处理器注册器契约。 diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs similarity index 99% rename from GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs rename to GFramework.Cqrs/Internal/CqrsDispatcher.cs index 0cae34c4..dafea402 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -7,7 +7,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Internal; +namespace GFramework.Cqrs.Internal; /// /// GFramework 自有 CQRS 运行时分发器。 diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs similarity index 99% rename from GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs rename to GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 65b1cbcf..85a95509 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -1,10 +1,9 @@ using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Internal; +namespace GFramework.Cqrs.Internal; /// /// 在架构初始化期间扫描并注册 CQRS 处理器。 diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs similarity index 96% rename from GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs rename to GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs index ddf0c06a..70d33562 100644 --- a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -3,7 +3,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Internal; +namespace GFramework.Cqrs.Internal; /// /// 默认的 CQRS 处理器注册器实现。 diff --git a/GFramework.Core/Cqrs/Notification/NotificationBase.cs b/GFramework.Cqrs/Notification/NotificationBase.cs similarity index 96% rename from GFramework.Core/Cqrs/Notification/NotificationBase.cs rename to GFramework.Cqrs/Notification/NotificationBase.cs index 05db2de7..06390406 100644 --- a/GFramework.Core/Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Cqrs/Notification/NotificationBase.cs @@ -14,7 +14,7 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Notification; -namespace GFramework.Core.Cqrs.Notification; +namespace GFramework.Cqrs.Notification; /// /// 表示一个基础通知类,用于处理带有输入的通知模式实现。 diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Cqrs/Query/QueryBase.cs similarity index 97% rename from GFramework.Core/Cqrs/Query/QueryBase.cs rename to GFramework.Cqrs/Query/QueryBase.cs index 759b8df1..d0f491b1 100644 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ b/GFramework.Cqrs/Query/QueryBase.cs @@ -13,7 +13,7 @@ using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Query; /// /// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。 diff --git a/GFramework.Core/Cqrs/Request/RequestBase.cs b/GFramework.Cqrs/Request/RequestBase.cs similarity index 97% rename from GFramework.Core/Cqrs/Request/RequestBase.cs rename to GFramework.Cqrs/Request/RequestBase.cs index 5ff18a04..8c534c86 100644 --- a/GFramework.Core/Cqrs/Request/RequestBase.cs +++ b/GFramework.Cqrs/Request/RequestBase.cs @@ -14,7 +14,7 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Request; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Request; /// /// 表示一个基础请求类,用于处理带有输入和响应的请求模式实现。 diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index 6805d2e7..15ee4561 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -1,10 +1,10 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Extensions; -using GFramework.Core.Extensions; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Extensions; namespace GFramework.Godot.Coroutine; diff --git a/GFramework.SourceGenerators.Common/Constants/PathContests.cs b/GFramework.SourceGenerators.Common/Constants/PathContests.cs index 3facd8b6..a9416fbe 100644 --- a/GFramework.SourceGenerators.Common/Constants/PathContests.cs +++ b/GFramework.SourceGenerators.Common/Constants/PathContests.cs @@ -15,6 +15,11 @@ public static class PathContests /// public const string CoreNamespace = $"{BaseNamespace}.Core"; + /// + /// GFramework CQRS runtime 命名空间 + /// + public const string CqrsNamespace = $"{BaseNamespace}.Cqrs"; + /// /// GFramework Godot模块命名空间 /// @@ -45,4 +50,9 @@ public static class PathContests /// GFramework核心抽象层命名空间 /// public const string CoreAbstractionsNamespace = $"{CoreNamespace}.Abstractions"; -} \ No newline at end of file + + /// + /// GFramework CQRS 抽象层命名空间 + /// + public const string CqrsAbstractionsNamespace = $"{CqrsNamespace}.Abstractions"; +} diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index e2b7ffb1..0392ac8a 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -38,7 +38,7 @@ public class CqrsHandlerRegistryGeneratorTests } } - namespace GFramework.Core.Abstractions.Cqrs + namespace GFramework.Cqrs.Abstractions.Cqrs { public interface IRequest { } public interface INotification { } @@ -47,7 +47,10 @@ public class CqrsHandlerRegistryGeneratorTests public interface IRequestHandler where TRequest : IRequest { } 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); @@ -62,7 +65,7 @@ public class CqrsHandlerRegistryGeneratorTests namespace TestApp { - using GFramework.Core.Abstractions.Cqrs; + using GFramework.Cqrs.Abstractions.Cqrs; public sealed record PingQuery() : IRequest; public sealed record DomainEvent() : INotification; @@ -78,11 +81,11 @@ public class CqrsHandlerRegistryGeneratorTests // #nullable enable - [assembly: global::GFramework.Core.Abstractions.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] namespace GFramework.Generated.Cqrs; - internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Core.Abstractions.Cqrs.ICqrsHandlerRegistry + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry { public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) { @@ -93,19 +96,19 @@ public class CqrsHandlerRegistryGeneratorTests global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, - typeof(global::GFramework.Core.Abstractions.Cqrs.IRequestHandler), + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(global::TestApp.AlphaQueryHandler)); - logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Core.Abstractions.Cqrs.IRequestHandler."); + logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, - typeof(global::GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler), + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(global::TestApp.StreamHandler)); - logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler."); + logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler."); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, - typeof(global::GFramework.Core.Abstractions.Cqrs.INotificationHandler), + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler), typeof(global::TestApp.ZetaNotificationHandler)); - logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Core.Abstractions.Cqrs.INotificationHandler."); + logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler."); } } @@ -143,7 +146,7 @@ public class CqrsHandlerRegistryGeneratorTests } } - namespace GFramework.Core.Abstractions.Cqrs + namespace GFramework.Cqrs.Abstractions.Cqrs { public interface IRequest { } public interface INotification { } @@ -152,7 +155,10 @@ public class CqrsHandlerRegistryGeneratorTests public interface IRequestHandler where TRequest : IRequest { } 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); @@ -167,7 +173,7 @@ public class CqrsHandlerRegistryGeneratorTests namespace TestApp { - using GFramework.Core.Abstractions.Cqrs; + using GFramework.Cqrs.Abstractions.Cqrs; public sealed record VisibleRequest() : IRequest; diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 39c3aadb..65aad69a 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -8,13 +8,17 @@ namespace GFramework.SourceGenerators.Cqrs; [Generator] public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { - private const string CqrsNamespace = $"{PathContests.CoreAbstractionsNamespace}.Cqrs"; + private const string CqrsContractsNamespace = $"{PathContests.CqrsAbstractionsNamespace}.Cqrs"; + private const string CqrsRuntimeNamespace = PathContests.CqrsNamespace; private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging"; - private const string IRequestHandlerMetadataName = $"{CqrsNamespace}.IRequestHandler`2"; - private const string INotificationHandlerMetadataName = $"{CqrsNamespace}.INotificationHandler`1"; - private const string IStreamRequestHandlerMetadataName = $"{CqrsNamespace}.IStreamRequestHandler`2"; - private const string ICqrsHandlerRegistryMetadataName = $"{CqrsNamespace}.ICqrsHandlerRegistry"; - private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsNamespace}.CqrsHandlerRegistryAttribute"; + private const string IRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IRequestHandler`2"; + private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1"; + private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; + private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; + + private const string CqrsHandlerRegistryAttributeMetadataName = + $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; + private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger"; private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; private const string GeneratedNamespace = "GFramework.Generated.Cqrs"; @@ -273,7 +277,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine("#nullable enable"); builder.AppendLine(); builder.Append("[assembly: global::"); - builder.Append(CqrsNamespace); + builder.Append(CqrsRuntimeNamespace); builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); builder.Append(GeneratedNamespace); builder.Append('.'); @@ -287,7 +291,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append("internal sealed class "); builder.Append(GeneratedTypeName); builder.Append(" : global::"); - builder.Append(CqrsNamespace); + builder.Append(CqrsRuntimeNamespace); builder.AppendLine(".ICqrsHandlerRegistry"); builder.AppendLine("{"); builder.Append( diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index 57d4effd..f5913c6e 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -4,10 +4,11 @@ using System.Reflection; using GFramework.Core.Abstractions.Cqrs; 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; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Command; namespace GFramework.Tests.Common; @@ -20,7 +21,9 @@ namespace GFramework.Tests.Common; /// public static class CqrsTestRuntime { - private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + private static readonly Assembly CqrsRuntimeAssembly = typeof(CommandBase<,>).Assembly; + + private static readonly Type CqrsHandlerRegistrarType = CqrsRuntimeAssembly .GetType( "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; @@ -40,41 +43,6 @@ public static class CqrsTestRuntime ?? throw new InvalidOperationException( "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); - private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsDispatcher", - throwOnError: true)!; - - private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( - BindingFlags.Instance | - BindingFlags.Public | - BindingFlags.NonPublic, - binder: null, - [ - typeof(IIocContainer), - typeof(ILogger) - ], - modifiers: null) - ?? throw new InvalidOperationException( - "Failed to locate CqrsDispatcher constructor."); - - private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", - throwOnError: true)!; - - private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = - DefaultCqrsHandlerRegistrarType.GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - binder: null, - [ - typeof(IIocContainer), - typeof(ILogger) - ], - modifiers: null) - ?? throw new InvalidOperationException( - "Failed to locate DefaultCqrsHandlerRegistrar constructor."); - /// /// 为裸测试容器补齐默认 CQRS runtime seam。 /// @@ -92,16 +60,15 @@ public static class CqrsTestRuntime if (container.Get() is null) { - var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(CqrsDispatcherType.Name); - var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger); container.Register(runtime); } if (container.Get() is null) { - var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(DefaultCqrsHandlerRegistrarType.Name); - var registrar = - (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); + var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); container.Register(registrar); } } diff --git a/GFramework.Tests.Common/GFramework.Tests.Common.csproj b/GFramework.Tests.Common/GFramework.Tests.Common.csproj index 51f8fba9..ddd8c02c 100644 --- a/GFramework.Tests.Common/GFramework.Tests.Common.csproj +++ b/GFramework.Tests.Common/GFramework.Tests.Common.csproj @@ -10,6 +10,7 @@ + From e36c80229a8719de411a7a4be42a2b3d947fd90f Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:34:08 +0800 Subject: [PATCH 39/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E5=9F=BA=E7=B1=BB=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=BF=90=E8=A1=8C=E6=97=B6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入AbstractCommandHandler、AbstractQueryHandler等各类处理器基类 - 实现CqrsContextAwareHandlerBase提供上下文感知功能 - 添加CqrsTestRuntime为测试项目提供CQRS运行时访问入口 - 创建AbstractCqrsHandlerContextTests验证上下文注入行为 - 支持命令、查询、通知及流式处理的各种抽象基类实现 --- .../Cqrs/AbstractCqrsHandlerContextTests.cs | 74 +++++++++++++++++++ .../Cqrs/Command/AbstractCommandHandler.cs | 12 +-- .../Command/AbstractStreamCommandHandler.cs | 7 +- .../Cqrs/CqrsContextAwareHandlerBase.cs | 59 +++++++++++++++ .../AbstractNotificationHandler.cs | 8 +- .../Cqrs/Query/AbstractQueryHandler.cs | 8 +- .../Cqrs/Query/AbstractStreamQueryHandler.cs | 7 +- .../Cqrs/Request/AbstractRequestHandler.cs | 11 ++- .../Request/AbstractStreamRequestHandler.cs | 7 +- GFramework.Tests.Common/CqrsTestRuntime.cs | 2 +- 10 files changed, 162 insertions(+), 33 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Command/AbstractCommandHandler.cs (83%) rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Command/AbstractStreamCommandHandler.cs (92%) create mode 100644 GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Notification/AbstractNotificationHandler.cs (85%) rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Query/AbstractQueryHandler.cs (83%) rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Query/AbstractStreamQueryHandler.cs (89%) rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Request/AbstractRequestHandler.cs (86%) rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Request/AbstractStreamRequestHandler.cs (88%) diff --git a/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs b/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs new file mode 100644 index 00000000..58e86f89 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs @@ -0,0 +1,74 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Cqrs.Command; + +namespace GFramework.Cqrs.Tests.Cqrs; + +/// +/// 验证 CQRS handler 基类在脱离 dispatcher 使用时会显式失败,并在注入上下文后保持可观察行为。 +/// +[TestFixture] +internal sealed class AbstractCqrsHandlerContextTests +{ + /// + /// 验证新的轻量 handler 基类不会再偷偷回退到全局 GameContext。 + /// + [Test] + public void GetContext_Should_Throw_When_Handler_Has_Not_Been_Initialized_By_Runtime() + { + var handler = new TestCommandHandler(); + + var exception = Assert.Throws(() => ((IContextAware)handler).GetContext()); + + Assert.That( + exception!.Message, + Does.Contain("has not been initialized").IgnoreCase); + } + + /// + /// 验证 runtime 注入上下文后,派生 handler 可以继续访问 Context 并收到 OnContextReady 回调。 + /// + [Test] + public async Task Handle_Should_Observe_Injected_Context_And_OnContextReady_Callback() + { + var handler = new TestCommandHandler(); + var context = new Mock(MockBehavior.Strict).Object; + + ((IContextAware)handler).SetContext(context); + await handler.Handle(new TestCommand(), CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(handler.OnContextReadyCallCount, Is.EqualTo(1)); + Assert.That(handler.LastObservedContext, Is.SameAs(context)); + }); + } + + /// + /// 用于验证上下文注入行为的最小 CQRS 命令。 + /// + private sealed record TestCommand : ICommand; + + /// + /// 暴露基类上下文访问与初始化回调的测试处理器。 + /// + private sealed class TestCommandHandler : AbstractCommandHandler + { + public int OnContextReadyCallCount { get; private set; } + + public IArchitectureContext? LastObservedContext { get; private set; } + + protected override void OnContextReady() + { + OnContextReadyCallCount++; + } + + public override ValueTask Handle(TestCommand command, CancellationToken cancellationToken) + { + LastObservedContext = Context; + return ValueTask.FromResult(Unit.Value); + } + } +} diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs similarity index 83% rename from GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs rename to GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs index d7ebb117..825737bf 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs @@ -11,19 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Cqrs.Command; /// /// 抽象命令处理器基类 -/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 /// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 命令类型 -public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractCommandHandler : CqrsContextAwareHandlerBase, IRequestHandler where TCommand : ICommand { /// @@ -38,12 +37,13 @@ public abstract class AbstractCommandHandler : ContextAwareBase, IRequ /// /// 抽象命令处理器基类(带返回值版本) -/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 /// 支持泛型命令和结果类型,框架会在每次分发前注入当前架构上下文。 /// /// 命令类型,必须实现ICommand接口 /// 命令执行结果类型 -public abstract class AbstractCommandHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractCommandHandler : CqrsContextAwareHandlerBase, + IRequestHandler where TCommand : ICommand { /// diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs similarity index 92% rename from GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs rename to GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs index 223a9cc5..cb4ccb6b 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -11,15 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Cqrs.Command; /// /// 抽象流式命令处理器基类。 -/// 继承自 并实现 , +/// 继承自轻量 CQRS 上下文基类并实现 , /// 为具体的流式命令处理器提供基础功能。 /// /// 流式命令类型,必须实现 @@ -32,7 +31,7 @@ namespace GFramework.Core.Cqrs.Command; /// 传入 的取消令牌同时约束流的创建与后续枚举, /// 派生类应在启动阶段和每次生成响应前尊重取消请求,避免在调用方停止枚举后继续执行后台工作。 /// -public abstract class AbstractStreamCommandHandler : ContextAwareBase, +public abstract class AbstractStreamCommandHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TCommand : IStreamCommand { diff --git a/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs b/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs new file mode 100644 index 00000000..73e25ff1 --- /dev/null +++ b/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs @@ -0,0 +1,59 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Cqrs.Cqrs; + +/// +/// 为 CQRS 处理器提供最小化的上下文感知基类实现。 +/// +/// +/// 该基类只承接 CQRS runtime 在分发前注入的 , +/// 不再像 ContextAwareBase 那样回退到 GameContext 全局查找。 +/// 这样可以让 GFramework.Cqrs 保持对 GFramework.Core 运行时实现的零依赖, +/// 同时在处理器被错误地脱离 dispatcher 使用时以显式异常快速失败。 +/// +public abstract class CqrsContextAwareHandlerBase : IContextAware +{ + private IArchitectureContext? _context; + + /// + /// 获取当前分发周期内已注入的架构上下文。 + /// + /// + /// 当前处理器尚未被 CQRS runtime 注入上下文。 + /// + protected IArchitectureContext Context => _context ?? throw new InvalidOperationException( + "The CQRS handler context has not been initialized. Ensure the handler is invoked through the CQRS runtime."); + + /// + /// 由 runtime 在分发前注入当前架构上下文。 + /// + /// 当前架构上下文。 + void IContextAware.SetContext(IArchitectureContext context) + { + ArgumentNullException.ThrowIfNull(context); + + _context = context; + OnContextReady(); + } + + /// + /// 获取当前处理器实例已绑定的架构上下文。 + /// + /// 当前分发周期内的架构上下文。 + IArchitectureContext IContextAware.GetContext() + { + return Context; + } + + /// + /// 当上下文注入完成后执行额外初始化。 + /// + /// + /// 该钩子保留与旧 ContextAwareBase 相近的扩展点, + /// 便于处理器在迁移后继续承接分发前的派生类初始化逻辑。 + /// + protected virtual void OnContextReady() + { + } +} diff --git a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs b/GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs similarity index 85% rename from GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs rename to GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs index da6f5281..16246d16 100644 --- a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs +++ b/GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs @@ -11,18 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Notification; +namespace GFramework.Cqrs.Cqrs.Notification; /// /// 抽象通知处理器基类 -/// 继承自ContextAwareBase并实现INotificationHandler接口,为具体的通知处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现INotificationHandler接口,为具体的通知处理器提供基础功能 /// 用于处理CQRS模式中的通知消息,支持异步处理 /// /// 通知类型,必须实现INotification接口 -public abstract class AbstractNotificationHandler : ContextAwareBase, INotificationHandler +public abstract class AbstractNotificationHandler : CqrsContextAwareHandlerBase, + INotificationHandler where TNotification : INotification { /// diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs similarity index 83% rename from GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs rename to GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs index 85c86425..5096f4b7 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs @@ -11,20 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Cqrs.Query; /// /// 抽象查询处理器基类 -/// 继承自 ContextAwareBase 并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。 /// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 查询类型,必须实现IQuery接口 /// 查询结果类型 -public abstract class AbstractQueryHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractQueryHandler : CqrsContextAwareHandlerBase, + IRequestHandler where TQuery : IQuery { /// diff --git a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs similarity index 89% rename from GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs rename to GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs index 9695dc42..7d301009 100644 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -11,20 +11,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Cqrs.Query; /// /// 抽象流式查询处理器基类 -/// 继承自ContextAwareBase并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 /// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景 /// /// 流式查询类型,必须实现IStreamQuery接口 /// 流式查询响应元素类型 -public abstract class AbstractStreamQueryHandler : ContextAwareBase, +public abstract class AbstractStreamQueryHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TQuery : IStreamQuery { diff --git a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs b/GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs similarity index 86% rename from GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs rename to GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs index f26c7cfa..8343576b 100644 --- a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs +++ b/GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs @@ -11,17 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Cqrs.Request; /// /// 抽象请求处理器基类,用于处理不返回具体响应的请求 -/// 继承自ContextAwareBase并实现IRequestHandler接口 +/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[Unit]接口 -public abstract class AbstractRequestHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler where TRequest : IRequest { /// @@ -35,11 +34,11 @@ public abstract class AbstractRequestHandler : ContextAwareBase, IRequ /// /// 抽象请求处理器基类,用于处理需要返回具体响应的请求 -/// 继承自ContextAwareBase并实现IRequestHandler接口 +/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[TResponse]接口 /// 响应类型 -public abstract class AbstractRequestHandler : ContextAwareBase, +public abstract class AbstractRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler where TRequest : IRequest { /// diff --git a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs b/GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs similarity index 88% rename from GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs rename to GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs index 2cbf438d..42b968f4 100644 --- a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs +++ b/GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs @@ -11,19 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Cqrs.Request; /// /// 抽象流式请求处理器基类 -/// 继承自ContextAwareBase并实现IStreamRequestHandler接口,为具体的流式请求处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现IStreamRequestHandler接口,为具体的流式请求处理器提供基础功能 /// 支持流式处理请求并产生异步可枚举的响应序列,适用于需要逐步返回结果的请求处理场景 /// /// 流式请求类型,必须实现IStreamRequest接口 /// 流式请求响应元素类型 -public abstract class AbstractStreamRequestHandler : ContextAwareBase, +public abstract class AbstractStreamRequestHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TRequest : IStreamRequest { diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index f5913c6e..f044bbc8 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -25,7 +25,7 @@ public static class CqrsTestRuntime private static readonly Type CqrsHandlerRegistrarType = CqrsRuntimeAssembly .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + "GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType From 1c7558aeb829970e8a7bd063f3ed4145fc2d57ab Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:40:58 +0800 Subject: [PATCH 40/70] =?UTF-8?q?refactor(cqrs):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=97=A7=E6=97=A5=E5=BF=97=E8=A1=8C=E4=B8=BA=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0CQRS=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 LoggingBehavior 类及其相关实现 - 新增 CqrsRuntimeModule 用于注册CQRS运行时组件 - 添加 ArchitectureComponentRegistryBehaviorTests 测试组件注册行为 - 添加 ArchitectureContextTests 测试架构上下文功能 - 完善CQRS运行时的并发安全性和生命周期管理 --- .../Logging/LoggerFactoryResolver.cs | 250 ++++++++++++++++++ ...hitectureComponentRegistryBehaviorTests.cs | 4 +- .../Architectures/ArchitectureContextTests.cs | 1 + ...ArchitectureInitializationPipelineTests.cs | 4 +- .../ArchitectureLifecycleBehaviorTests.cs | 4 +- .../ArchitectureModulesBehaviorTests.cs | 1 + .../Architectures/PriorityServiceTests.cs | 3 +- .../Ioc/MicrosoftDiContainerTests.cs | 1 + .../State/StateMachineSystemTests.cs | 3 +- .../Logging/LoggerFactoryResolver.cs | 26 -- GFramework.Core/Properties/TypeForwarders.cs | 4 + .../Services/Modules/CqrsRuntimeModule.cs | 2 +- GFramework.Cqrs.Tests/GlobalUsings.cs | 1 + .../Mediator/MediatorAdvancedFeaturesTests.cs | 1 + .../MediatorArchitectureIntegrationTests.cs | 1 + .../Mediator/MediatorComprehensiveTests.cs | 1 + .../Cqrs/Behaviors/LoggingBehavior.cs | 26 +- .../Cqrs/Behaviors/PerformanceBehavior.cs | 32 +-- GFramework.Cqrs/GlobalUsings.cs | 1 + 19 files changed, 302 insertions(+), 64 deletions(-) create mode 100644 GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs delete mode 100644 GFramework.Core/Logging/LoggerFactoryResolver.cs create mode 100644 GFramework.Core/Properties/TypeForwarders.cs rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Behaviors/LoggingBehavior.cs (71%) rename {GFramework.Core => GFramework.Cqrs}/Cqrs/Behaviors/PerformanceBehavior.cs (61%) diff --git a/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs new file mode 100644 index 00000000..a87a18da --- /dev/null +++ b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs @@ -0,0 +1,250 @@ +namespace GFramework.Core.Abstractions.Logging; + +/// +/// 提供全局日志工厂访问入口。 +/// +/// +/// 该类型位于抽象层,是为了让上层模块可以在不依赖 GFramework.Core 实现程序集的前提下 +/// 获取日志记录器。默认 provider 会优先通过反射解析 GFramework.Core 中的控制台实现, +/// 若宿主未加载该程序集,则退回到静默 provider,避免抽象层形成实现层循环依赖。 +/// +public static class LoggerFactoryResolver +{ + private const string DefaultProviderTypeName = + "GFramework.Core.Logging.ConsoleLoggerFactoryProvider, GFramework.Core"; + + /// + /// 获取或设置当前日志工厂提供程序。 + /// + /// + /// 当赋值为 时抛出。 + /// + public static ILoggerFactoryProvider Provider + { + get => field ??= CreateDefaultProvider(); + set => field = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// 获取或设置新创建日志记录器的最小日志级别。 + /// + /// + /// 该属性直接代理到当前 ,确保调用方调整级别后立即影响后续创建的日志器。 + /// + public static LogLevel MinLevel + { + get => Provider.MinLevel; + set => Provider.MinLevel = value; + } + + private static ILoggerFactoryProvider CreateDefaultProvider() + { + if (Type.GetType(DefaultProviderTypeName, throwOnError: false) is { } providerType && + Activator.CreateInstance(providerType) is ILoggerFactoryProvider provider) + { + provider.MinLevel = LogLevel.Info; + return provider; + } + + return new SilentLoggerFactoryProvider(); + } + + /// + /// 当宿主未提供默认日志实现时使用的静默 provider。 + /// + private sealed class SilentLoggerFactoryProvider : ILoggerFactoryProvider + { + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + public ILogger CreateLogger(string name) + { + return new SilentLogger(name); + } + } + + /// + /// 默认日志实现不可用时的 no-op 日志器。 + /// + private sealed class SilentLogger(string name) : ILogger + { + public string Name() + { + return name; + } + + public bool IsTraceEnabled() + { + return false; + } + + public bool IsDebugEnabled() + { + return false; + } + + public bool IsInfoEnabled() + { + return false; + } + + public bool IsWarnEnabled() + { + return false; + } + + public bool IsErrorEnabled() + { + return false; + } + + public bool IsFatalEnabled() + { + return false; + } + + public bool IsEnabledForLevel(LogLevel level) + { + return false; + } + + public void Trace(string msg) + { + } + + public void Trace(string format, object arg) + { + } + + public void Trace(string format, object arg1, object arg2) + { + } + + public void Trace(string format, params object[] arguments) + { + } + + public void Trace(string msg, Exception t) + { + } + + public void Debug(string msg) + { + } + + public void Debug(string format, object arg) + { + } + + public void Debug(string format, object arg1, object arg2) + { + } + + public void Debug(string format, params object[] arguments) + { + } + + public void Debug(string msg, Exception t) + { + } + + public void Info(string msg) + { + } + + public void Info(string format, object arg) + { + } + + public void Info(string format, object arg1, object arg2) + { + } + + public void Info(string format, params object[] arguments) + { + } + + public void Info(string msg, Exception t) + { + } + + public void Warn(string msg) + { + } + + public void Warn(string format, object arg) + { + } + + public void Warn(string format, object arg1, object arg2) + { + } + + public void Warn(string format, params object[] arguments) + { + } + + public void Warn(string msg, Exception t) + { + } + + public void Error(string msg) + { + } + + public void Error(string format, object arg) + { + } + + public void Error(string format, object arg1, object arg2) + { + } + + public void Error(string format, params object[] arguments) + { + } + + public void Error(string msg, Exception t) + { + } + + public void Fatal(string msg) + { + } + + public void Fatal(string format, object arg) + { + } + + public void Fatal(string format, object arg1, object arg2) + { + } + + public void Fatal(string format, params object[] arguments) + { + } + + public void Fatal(string msg, Exception t) + { + } + + public void Log(LogLevel level, string message) + { + } + + public void Log(LogLevel level, string format, object arg) + { + } + + public void Log(LogLevel level, string format, object arg1, object arg2) + { + } + + public void Log(LogLevel level, string format, params object[] arguments) + { + } + + public void Log(LogLevel level, string message, Exception exception) + { + } + } +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs index 583c30c8..f6cbdafa 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs @@ -1,11 +1,11 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -714,4 +714,4 @@ public class ArchitectureComponentRegistryBehaviorTests return _context; } } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 87978cd2..cdcde44d 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -5,6 +5,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs index 717e1b18..10945ad2 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs @@ -1,9 +1,9 @@ using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Events; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Environment; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -185,4 +185,4 @@ public class ArchitectureInitializationPipelineTests private sealed class BootstrapMarker { } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs index b0ef7262..943e2bfd 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs @@ -2,12 +2,12 @@ using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Lifecycle; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using Microsoft.Extensions.DependencyInjection; namespace GFramework.Core.Tests.Architectures; @@ -460,4 +460,4 @@ public class ArchitectureLifecycleBehaviorTests return _context; } } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index cfe0db79..493f3590 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -1,4 +1,5 @@ using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; diff --git a/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs b/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs index 24fd2681..0499a4ab 100644 --- a/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs +++ b/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; @@ -244,4 +245,4 @@ public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized public int Priority => 30; } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 0e03059e..4104ccb1 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,6 +1,7 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; diff --git a/GFramework.Core.Tests/State/StateMachineSystemTests.cs b/GFramework.Core.Tests/State/StateMachineSystemTests.cs index 0ea62de6..2c234238 100644 --- a/GFramework.Core.Tests/State/StateMachineSystemTests.cs +++ b/GFramework.Core.Tests/State/StateMachineSystemTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Enums; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.State; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Architectures; @@ -373,4 +374,4 @@ public class TestStateV5_2 : IState } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core/Logging/LoggerFactoryResolver.cs b/GFramework.Core/Logging/LoggerFactoryResolver.cs deleted file mode 100644 index c15378d1..00000000 --- a/GFramework.Core/Logging/LoggerFactoryResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using GFramework.Core.Abstractions.Logging; - -namespace GFramework.Core.Logging; - -/// -/// 日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例 -/// -public static class LoggerFactoryResolver -{ - /// - /// 获取或设置当前的日志工厂提供程序 - /// - /// - /// 日志工厂提供程序实例,默认为控制台日志工厂提供程序 - /// - public static ILoggerFactoryProvider Provider { get; set; } - = new ConsoleLoggerFactoryProvider(); - - /// - /// 获取或设置日志记录的最小级别 - /// - /// - /// 日志级别枚举值,默认为Info级别 - /// - public static LogLevel MinLevel { get; set; } = LogLevel.Info; -} \ No newline at end of file diff --git a/GFramework.Core/Properties/TypeForwarders.cs b/GFramework.Core/Properties/TypeForwarders.cs new file mode 100644 index 00000000..a71d9af2 --- /dev/null +++ b/GFramework.Core/Properties/TypeForwarders.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +using GFramework.Core.Abstractions.Logging; + +[assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))] diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 9e7ad307..3fe37558 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -1,7 +1,7 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Logging; +using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs index c47473b0..9f52a3dd 100644 --- a/GFramework.Cqrs.Tests/GlobalUsings.cs +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -20,6 +20,7 @@ global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Threading; global using System.Threading.Tasks; +global using System.Diagnostics; global using GFramework.Tests.Common; global using Microsoft.Extensions.DependencyInjection; global using Moq; diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index 257809f0..fe8f4413 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -1,3 +1,4 @@ +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index e403735e..728f005a 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,4 +1,5 @@ using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Ioc; diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index b0b510d6..423b1c9b 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -1,5 +1,6 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Events; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Environment; diff --git a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs similarity index 71% rename from GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs rename to GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs index 7230f53d..ccd9f0bf 100644 --- a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs @@ -11,19 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Behaviors; +namespace GFramework.Cqrs.Cqrs.Behaviors; /// -/// 日志记录行为类,用于在CQRS管道中记录请求处理的日志信息 -/// 实现IPipelineBehavior接口,为请求处理提供日志记录功能 +/// 在 CQRS 请求管道中记录请求开始、完成、取消与失败日志。 /// -/// 请求类型,必须实现IRequest接口 -/// 响应类型 +/// 请求类型。 +/// 响应类型。 +/// +/// 该行为保留在 GFramework.Core.Cqrs.Behaviors 命名空间以兼容现有调用点, +/// 但实现已迁入 GFramework.Cqrs 程序集,避免继续由 GFramework.Core 承载 CQRS runtime 细节。 +/// public sealed class LoggingBehavior : IPipelineBehavior where TRequest : IRequest { @@ -31,13 +32,12 @@ public sealed class LoggingBehavior : IPipelineBehavior)); /// - /// 处理请求并记录日志 - /// 在请求处理前后记录调试信息,处理异常时记录错误日志 + /// 执行日志包装后的下一段请求处理逻辑。 /// - /// 要处理的请求消息 - /// 下一个处理委托,用于继续管道执行 - /// 取消令牌,用于取消操作 - /// 处理结果的ValueTask + /// 当前请求消息。 + /// 后续处理委托。 + /// 取消令牌。 + /// 请求处理结果。 public async ValueTask Handle( TRequest message, MessageHandlerDelegate next, diff --git a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/PerformanceBehavior.cs similarity index 61% rename from GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs rename to GFramework.Cqrs/Cqrs/Behaviors/PerformanceBehavior.cs index 35ab2978..1d13319d 100644 --- a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/PerformanceBehavior.cs @@ -11,33 +11,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Behaviors; +namespace GFramework.Cqrs.Cqrs.Behaviors; /// -/// 性能监控行为类,用于监控CQRS请求的执行时间 -/// 实现IPipelineBehavior接口,检测并记录执行时间过长的请求 +/// 在 CQRS 请求管道中监控处理耗时,并对长耗时请求发出告警。 /// -/// 请求类型,必须实现IRequest接口 -/// 响应类型 +/// 请求类型。 +/// 响应类型。 +/// +/// 该行为保留现有公开命名空间以维持消费端兼容性,但实现已迁入 GFramework.Cqrs 程序集。 +/// public sealed class PerformanceBehavior : IPipelineBehavior where TRequest : IRequest { + private const double SlowRequestThresholdMilliseconds = 500; + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PerformanceBehavior)); /// - /// 处理请求并监控执行时间 - /// 使用Stopwatch测量请求处理耗时,超过500ms时记录警告日志 + /// 统计当前请求处理耗时,并在超阈值时记录警告日志。 /// - /// 要处理的请求消息 - /// 下一个处理委托,用于继续管道执行 - /// 取消令牌,用于取消操作 - /// 处理结果的ValueTask + /// 当前请求消息。 + /// 后续处理委托。 + /// 取消令牌。 + /// 请求处理结果。 public async ValueTask Handle( TRequest message, MessageHandlerDelegate next, @@ -53,11 +54,10 @@ public sealed class PerformanceBehavior : IPipelineBehavior { var elapsed = Stopwatch.GetElapsedTime(start); - if (elapsed.TotalMilliseconds > 500) + if (elapsed.TotalMilliseconds > SlowRequestThresholdMilliseconds) { var requestName = typeof(TRequest).Name; - _logger.Warn( - $"Long Running Request: {requestName} ({elapsed.TotalMilliseconds:F2} ms)"); + _logger.Warn($"Long Running Request: {requestName} ({elapsed.TotalMilliseconds:F2} ms)"); } } } diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs index 7f8c14d6..97f2d13a 100644 --- a/GFramework.Cqrs/GlobalUsings.cs +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -4,3 +4,4 @@ global using System.Linq; global using System.Threading; global using System.Threading.Tasks; global using Microsoft.Extensions.DependencyInjection; +global using System.Diagnostics; From 18337c59952466b91f2c7f02d9aeb7755ac71099 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:54:29 +0800 Subject: [PATCH 41/70] =?UTF-8?q?refactor(tests):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20System.Diagnostics=20?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 GlobalUsings.cs 中删除未使用的 System.Diagnostics 全局引用 - 保持测试项目的全局引用列表整洁 - 减少不必要的命名空间导入 --- GFramework.Cqrs.Tests/GlobalUsings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs index 9f52a3dd..c47473b0 100644 --- a/GFramework.Cqrs.Tests/GlobalUsings.cs +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -20,7 +20,6 @@ global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Threading; global using System.Threading.Tasks; -global using System.Diagnostics; global using GFramework.Tests.Common; global using Microsoft.Extensions.DependencyInjection; global using Moq; From 005c32d84f7ca0fedf6a46eafe8a6f89841a5d2e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:27:09 +0800 Subject: [PATCH 42/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E6=96=B9=E6=B3=95=E5=85=BC=E5=AE=B9=E5=B1=82?= =?UTF-8?q?=E5=92=8C=E6=97=A5=E5=BF=97=E5=B7=A5=E5=8E=82=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增ContextAwareMediatorCommandExtensions提供命令扩展方法兼容性支持 - 新增ContextAwareMediatorQueryExtensions提供查询扩展方法兼容性支持 - 添加LoggerFactoryResolver实现全局日志工厂访问入口 - 实现TypeForwarders将核心类型转发到正确程序集 - 添加MediatorCompatibilityDeprecationTests验证弃用策略 - 扩展LoggerFactoryTests覆盖并发初始化和回退逻辑 - 迁移CommandBase到Core.Cqrs.Command命名空间 - 移动LoggingBehavior到GFramework.Cqrs.Cqrs.Behaviors - 添加AbstractStreamQueryHandler支持流式查询处理 - 创建NotificationBase提供通知基类实现 --- .../Logging/LoggerFactoryResolver.cs | 45 +++- .../CqrsPublicNamespaceCompatibilityTests.cs | 76 ++++++ .../MediatorCompatibilityDeprecationTests.cs | 4 +- .../Logging/LoggerFactoryTests.cs | 238 ++++++++++++++++-- .../ContextAwareMediatorCommandExtensions.cs | 2 +- .../ContextAwareMediatorQueryExtensions.cs | 2 +- GFramework.Core/Properties/TypeForwarders.cs | 8 + GFramework.Cqrs/Command/CommandBase.cs | 20 +- .../Cqrs/Behaviors/LoggingBehavior.cs | 4 +- .../Cqrs/Query/AbstractStreamQueryHandler.cs | 22 +- GFramework.Cqrs/CqrsRuntimeFactory.cs | 6 + .../Notification/NotificationBase.cs | 18 +- GFramework.Cqrs/Query/QueryBase.cs | 20 +- GFramework.Cqrs/Request/RequestBase.cs | 20 +- GFramework.Tests.Common/CqrsTestRuntime.cs | 4 +- 15 files changed, 409 insertions(+), 80 deletions(-) create mode 100644 GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs diff --git a/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs index a87a18da..fdffe5b2 100644 --- a/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs +++ b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs @@ -10,19 +10,42 @@ namespace GFramework.Core.Abstractions.Logging; /// public static class LoggerFactoryResolver { - private const string DefaultProviderTypeName = + private static readonly object ProviderLock = new(); + + private static string DefaultProviderTypeName = "GFramework.Core.Logging.ConsoleLoggerFactoryProvider, GFramework.Core"; + private static ILoggerFactoryProvider? _provider; + /// /// 获取或设置当前日志工厂提供程序。 /// + /// + /// 读取与赋值都会通过同一把锁串行化,确保并发调用方观察到确定的 provider 引用。 + /// 当调用方未显式赋值时,会在首次访问时尝试解析默认实现;若解析失败,则退回静默 provider。 + /// /// /// 当赋值为 时抛出。 /// public static ILoggerFactoryProvider Provider { - get => field ??= CreateDefaultProvider(); - set => field = value ?? throw new ArgumentNullException(nameof(value)); + get + { + lock (ProviderLock) + { + _provider ??= CreateDefaultProvider(); + return _provider; + } + } + set + { + var provider = value ?? throw new ArgumentNullException(nameof(value)); + + lock (ProviderLock) + { + _provider = provider; + } + } } /// @@ -39,11 +62,19 @@ public static class LoggerFactoryResolver private static ILoggerFactoryProvider CreateDefaultProvider() { - if (Type.GetType(DefaultProviderTypeName, throwOnError: false) is { } providerType && - Activator.CreateInstance(providerType) is ILoggerFactoryProvider provider) + try { - provider.MinLevel = LogLevel.Info; - return provider; + if (Type.GetType(DefaultProviderTypeName, throwOnError: false) is { } providerType && + Activator.CreateInstance(providerType) is ILoggerFactoryProvider provider) + { + provider.MinLevel = LogLevel.Info; + return provider; + } + } + catch (Exception) + { + // The default provider is optional. Any load or activation failure must degrade to the silent provider so + // abstractions-only hosts can continue bootstrapping without the concrete logging assembly. } return new SilentLoggerFactoryProvider(); diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs new file mode 100644 index 00000000..2f02dbb7 --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -0,0 +1,76 @@ +using GFramework.Core.Cqrs.Command; +using GFramework.Core.Cqrs.Notification; +using GFramework.Core.Cqrs.Query; +using GFramework.Core.Cqrs.Request; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Notification; +using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Request; + +namespace GFramework.Core.Tests.Cqrs; + +/// +/// 锁定 CQRS 基础消息类型在 runtime 拆分后的公开命名空间与程序集兼容性。 +/// +[TestFixture] +public sealed class CqrsPublicNamespaceCompatibilityTests +{ + /// + /// 验证基础消息类型继续暴露在历史 Core.Cqrs 命名空间,同时由独立 runtime 程序集承载实现。 + /// + [Test] + public void Base_Message_Types_Should_Remain_In_Legacy_Namespaces_While_Living_In_Runtime_Assembly() + { + Assert.Multiple(() => + { + AssertLegacyType(typeof(CommandBase), "GFramework.Core.Cqrs.Command"); + AssertLegacyType(typeof(QueryBase), "GFramework.Core.Cqrs.Query"); + AssertLegacyType(typeof(RequestBase), "GFramework.Core.Cqrs.Request"); + AssertLegacyType(typeof(NotificationBase), "GFramework.Core.Cqrs.Notification"); + }); + } + + /// + /// 验证旧的 GFramework.Core 程序集限定名仍可解析到迁移后的 runtime 实现类型。 + /// + [Test] + public void GFramework_Core_Assembly_Should_Forward_Legacy_Base_Types_To_Runtime_Assembly() + { + Assert.Multiple(() => + { + AssertForwardedType("GFramework.Core.Cqrs.Command.CommandBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Core.Cqrs.Query.QueryBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Core.Cqrs.Request.RequestBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Core.Cqrs.Notification.NotificationBase`1, GFramework.Core"); + }); + } + + private static void AssertLegacyType(Type type, string expectedNamespace) + { + Assert.Multiple(() => + { + Assert.That(type.Namespace, Is.EqualTo(expectedNamespace)); + Assert.That(type.Assembly.GetName().Name, Is.EqualTo("GFramework.Cqrs")); + }); + } + + private static void AssertForwardedType(string assemblyQualifiedTypeName) + { + var resolvedType = Type.GetType(assemblyQualifiedTypeName, throwOnError: true); + + Assert.Multiple(() => + { + Assert.That(resolvedType, Is.Not.Null); + Assert.That(resolvedType!.Assembly.GetName().Name, Is.EqualTo("GFramework.Cqrs")); + }); + } + + private sealed record TestCommandInput : ICommandInput; + + private sealed record TestQueryInput : IQueryInput; + + private sealed record TestRequestInput : IRequestInput; + + private sealed record TestNotificationInput : INotificationInput; +} diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs index b25806f0..a6cc927f 100644 --- a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -38,10 +38,10 @@ public class MediatorCompatibilityDeprecationTests "Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorCommandExtensions), - "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead."); + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorQueryExtensions), - "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead."); + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead."); AssertLegacyType( typeof(MediatorCoroutineExtensions), "Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead."); diff --git a/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs b/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs index 8ba7ac91..d175b32f 100644 --- a/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs +++ b/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs @@ -1,18 +1,19 @@ using System.IO; +using System.Reflection; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; -using NUnit.Framework; namespace GFramework.Core.Tests.Logging; /// -/// 测试LoggerFactory相关功能的测试类 +/// 测试 LoggerFactory 相关功能的测试类。 /// [TestFixture] +[NonParallelizable] public class LoggerFactoryTests { /// - /// 测试ConsoleLoggerFactory的GetLogger方法是否返回ConsoleLogger实例 + /// 测试 ConsoleLoggerFactory 的 GetLogger 方法是否返回 ConsoleLogger 实例。 /// [Test] public void ConsoleLoggerFactory_GetLogger_ShouldReturnConsoleLogger() @@ -26,7 +27,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory使用不同名称获取不同的logger实例 + /// 测试 ConsoleLoggerFactory 使用不同名称获取不同的 logger 实例。 /// [Test] public void ConsoleLoggerFactory_GetLogger_WithDifferentNames_ShouldReturnDifferentLoggers() @@ -40,7 +41,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory使用默认最小级别时的行为(默认为Info级别) + /// 测试 ConsoleLoggerFactory 使用默认最小级别时的行为。 /// [Test] public void ConsoleLoggerFactory_GetLogger_WithDefaultMinLevel_ShouldUseInfo() @@ -51,7 +52,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Info, stringWriter, false); - // 验证Debug消息不会被记录,但Info消息会被记录 testLogger.Debug("Debug message"); testLogger.Info("Info message"); @@ -61,7 +61,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider创建logger时使用提供者的最小级别设置 + /// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供者的最小级别设置。 /// [Test] public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldReturnLoggerWithProviderMinLevel() @@ -72,7 +72,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Debug, stringWriter, false); - // 验证Debug消息会被记录,但Trace消息不会被记录 testLogger.Debug("Debug message"); testLogger.Trace("Trace message"); @@ -82,7 +81,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider创建logger时使用提供的名称 + /// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供的名称。 /// [Test] public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldUseProvidedName() @@ -94,7 +93,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider属性是否有默认值 + /// 测试 LoggerFactoryResolver 的 Provider 属性是否有默认值。 /// [Test] public void LoggerFactoryResolver_Provider_ShouldHaveDefaultValue() @@ -104,7 +103,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider属性可以被更改 + /// 测试 LoggerFactoryResolver 的 Provider 属性可以被更改。 /// [Test] public void LoggerFactoryResolver_Provider_CanBeChanged() @@ -120,7 +119,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性是否有默认值 + /// 测试 LoggerFactoryResolver 的 MinLevel 属性是否有默认值。 /// [Test] public void LoggerFactoryResolver_MinLevel_ShouldHaveDefaultValue() @@ -129,7 +128,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性可以被更改 + /// 测试 LoggerFactoryResolver 的 MinLevel 属性可以被更改。 /// [Test] public void LoggerFactoryResolver_MinLevel_CanBeChanged() @@ -144,7 +143,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel属性是否有默认值 + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 属性是否有默认值。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_ShouldHaveDefaultValue() @@ -155,7 +154,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel属性可以被更改 + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 属性可以被更改。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_CanBeChanged() @@ -168,7 +167,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider创建logger时使用提供者设置 + /// 测试 LoggerFactoryResolver 的 Provider 创建 logger 时使用提供者设置。 /// [Test] public void LoggerFactoryResolver_Provider_CreateLogger_ShouldUseProviderSettings() @@ -183,7 +182,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Warning, stringWriter, false); - // 验证Warn消息会被记录,但Info消息不会被记录 testLogger.Warn("Warn message"); testLogger.Info("Info message"); @@ -195,7 +193,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性影响新创建的logger + /// 测试 LoggerFactoryResolver 的 MinLevel 属性影响新创建的 logger。 /// [Test] public void LoggerFactoryResolver_MinLevel_AffectsNewLoggers() @@ -210,7 +208,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Error, stringWriter, false); - // 验证Error消息会被记录,但Warn消息不会被记录 testLogger.Error("Error message"); testLogger.Warn("Warn message"); @@ -222,7 +219,93 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory创建的多个logger实例是独立的 + /// 验证默认 provider 激活失败时会回退到静默 provider。 + /// + [Test] + public void + LoggerFactoryResolver_Provider_Should_Fall_Back_To_SilentProvider_When_DefaultProvider_Activation_Fails() + { + var originalProvider = LoggerFactoryResolver.Provider; + var originalTypeName = GetDefaultProviderTypeName(); + + try + { + ResetProvider(); + SetDefaultProviderTypeName(typeof(ThrowingLoggerFactoryProvider).AssemblyQualifiedName!); + + var provider = LoggerFactoryResolver.Provider; + var logger = provider.CreateLogger("Fallback"); + + Assert.Multiple(() => + { + Assert.That(provider.GetType().Name, Is.EqualTo("SilentLoggerFactoryProvider")); + Assert.That(provider.MinLevel, Is.EqualTo(LogLevel.Info)); + Assert.That(logger.IsEnabledForLevel(LogLevel.Error), Is.False); + }); + } + finally + { + SetDefaultProviderTypeName(originalTypeName); + LoggerFactoryResolver.Provider = originalProvider; + } + } + + /// + /// 验证并发首次访问默认 provider 时只会创建一个实例,并向所有调用方返回相同引用。 + /// + [Test] + public async Task + LoggerFactoryResolver_Provider_Should_Create_A_Single_Default_Instance_When_Accessed_Concurrently() + { + var originalProvider = LoggerFactoryResolver.Provider; + var originalTypeName = GetDefaultProviderTypeName(); + + try + { + BlockingLoggerFactoryProvider.Reset(); + ResetProvider(); + SetDefaultProviderTypeName(typeof(BlockingLoggerFactoryProvider).AssemblyQualifiedName!); + + var startGate = new ManualResetEventSlim(false); + var tasks = Enumerable.Range(0, 8) + .Select(_ => Task.Run(() => + { + startGate.Wait(); + return LoggerFactoryResolver.Provider; + })) + .ToArray(); + + startGate.Set(); + + Assert.That( + SpinWait.SpinUntil( + () => BlockingLoggerFactoryProvider.ConstructionCount >= 1, + TimeSpan.FromSeconds(2)), + Is.True, + "The test provider should start construction after concurrent access begins."); + + BlockingLoggerFactoryProvider.ReleaseConstruction(); + + var providers = await Task.WhenAll(tasks); + + Assert.Multiple(() => + { + Assert.That(BlockingLoggerFactoryProvider.ConstructionCount, Is.EqualTo(1)); + Assert.That(providers.Distinct().Count(), Is.EqualTo(1)); + Assert.That(LoggerFactoryResolver.Provider, Is.SameAs(providers[0])); + }); + } + finally + { + BlockingLoggerFactoryProvider.ReleaseConstruction(); + BlockingLoggerFactoryProvider.Reset(); + SetDefaultProviderTypeName(originalTypeName); + LoggerFactoryResolver.Provider = originalProvider; + } + } + + /// + /// 测试 ConsoleLoggerFactory 创建的多个 logger 实例是独立的。 /// [Test] public void ConsoleLoggerFactory_MultipleLoggers_ShouldBeIndependent() @@ -236,7 +319,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel不会影响已创建的logger + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 不会影响已创建的 logger。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_DoesNotAffectCreatedLogger() @@ -247,7 +330,6 @@ public class LoggerFactoryTests var stringWriter = new StringWriter(); var testLogger = new ConsoleLogger("TestLogger", LogLevel.Error, stringWriter, false); - // 验证Error和Fatal消息都会被记录 testLogger.Error("Error message"); testLogger.Fatal("Fatal message"); @@ -255,4 +337,114 @@ public class LoggerFactoryTests Assert.That(output, Does.Contain("Error message")); Assert.That(output, Does.Contain("Fatal message")); } -} \ No newline at end of file + + private static string GetDefaultProviderTypeName() + { + return (string)GetResolverField("DefaultProviderTypeName").GetValue(null)!; + } + + private static void SetDefaultProviderTypeName(string typeName) + { + GetResolverField("DefaultProviderTypeName").SetValue(null, typeName); + } + + private static void ResetProvider() + { + GetResolverField("_provider").SetValue(null, null); + } + + private static FieldInfo GetResolverField(string fieldName) + { + return typeof(LoggerFactoryResolver).GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException( + $"Failed to locate LoggerFactoryResolver.{fieldName}."); + } + + /// + /// 用于触发默认 provider 激活失败回退路径的测试桩。 + /// + public sealed class ThrowingLoggerFactoryProvider : ILoggerFactoryProvider + { + /// + /// 初始化一个始终抛出异常的 provider。 + /// + /// 始终抛出,用于覆盖回退路径。 + public ThrowingLoggerFactoryProvider() + { + throw new InvalidOperationException("Simulated provider activation failure."); + } + + /// + /// 获取或设置最小日志级别。 + /// + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + /// + /// 创建日志器。 + /// + /// 日志器名称。 + /// 该测试桩永远不会成功创建日志器。 + /// 始终抛出,因为该方法不应被调用。 + public ILogger CreateLogger(string name) + { + throw new NotSupportedException(); + } + } + + /// + /// 用于验证并发首次初始化路径只创建单个 provider 实例的测试桩。 + /// + public sealed class BlockingLoggerFactoryProvider : ILoggerFactoryProvider + { + private static int _constructionCount; + private static ManualResetEventSlim _constructionGate = new(false); + + /// + /// 初始化一个会阻塞构造完成的 provider,用于放大并发首次访问竞争窗口。 + /// + public BlockingLoggerFactoryProvider() + { + Interlocked.Increment(ref _constructionCount); + _constructionGate.Wait(TimeSpan.FromSeconds(5)); + } + + /// + /// 获取已经发生的构造次数。 + /// + public static int ConstructionCount => Volatile.Read(ref _constructionCount); + + /// + /// 获取或设置最小日志级别。 + /// + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + /// + /// 创建测试日志器。 + /// + /// 日志器名称。 + /// 带有当前最小级别设置的控制台日志器。 + public ILogger CreateLogger(string name) + { + return new ConsoleLogger(name, MinLevel, TextWriter.Null, false); + } + + /// + /// 重置该测试桩的并发观测状态。 + /// + public static void Reset() + { + _constructionGate = new ManualResetEventSlim(false); + Interlocked.Exchange(ref _constructionCount, 0); + } + + /// + /// 释放当前被阻塞的 provider 构造过程。 + /// + public static void ReleaseConstruction() + { + _constructionGate.Set(); + } + } +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index d001cb71..24490239 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -12,7 +12,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorCommandExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index 4eb1b2c7..cf0b4513 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -12,7 +12,7 @@ namespace GFramework.Core.Extensions; /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorQueryExtensions { /// diff --git a/GFramework.Core/Properties/TypeForwarders.cs b/GFramework.Core/Properties/TypeForwarders.cs index a71d9af2..a27d2bf4 100644 --- a/GFramework.Core/Properties/TypeForwarders.cs +++ b/GFramework.Core/Properties/TypeForwarders.cs @@ -1,4 +1,12 @@ using System.Runtime.CompilerServices; using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Cqrs.Command; +using GFramework.Core.Cqrs.Notification; +using GFramework.Core.Cqrs.Query; +using GFramework.Core.Cqrs.Request; [assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))] +[assembly: TypeForwardedTo(typeof(CommandBase<,>))] +[assembly: TypeForwardedTo(typeof(QueryBase<,>))] +[assembly: TypeForwardedTo(typeof(RequestBase<,>))] +[assembly: TypeForwardedTo(typeof(NotificationBase<>))] diff --git a/GFramework.Cqrs/Command/CommandBase.cs b/GFramework.Cqrs/Command/CommandBase.cs index 57703c47..01351332 100644 --- a/GFramework.Cqrs/Command/CommandBase.cs +++ b/GFramework.Cqrs/Command/CommandBase.cs @@ -13,19 +13,23 @@ using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Cqrs.Command; +namespace GFramework.Core.Cqrs.Command; /// -/// 表示一个基础命令类,用于处理带有输入和响应的命令模式实现。 -/// 该类实现了 ICommand<TResponse> 接口,提供了通用的命令结构。 +/// 为携带输入模型的 CQRS 命令提供统一基类。 /// -/// 命令输入数据的类型 -/// 命令执行后返回结果的类型 -/// 命令执行所需的输入数据 -public abstract class CommandBase(TInput input) : ICommand where TInput : ICommandInput +/// 命令输入类型。 +/// 命令响应类型。 +/// 命令执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class CommandBase(TInput input) : ICommand + where TInput : ICommandInput { /// - /// 获取命令的输入数据。 + /// 获取命令执行时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs index ccd9f0bf..63896c92 100644 --- a/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs @@ -22,8 +22,8 @@ namespace GFramework.Cqrs.Cqrs.Behaviors; /// 请求类型。 /// 响应类型。 /// -/// 该行为保留在 GFramework.Core.Cqrs.Behaviors 命名空间以兼容现有调用点, -/// 但实现已迁入 GFramework.Cqrs 程序集,避免继续由 GFramework.Core 承载 CQRS runtime 细节。 +/// 该行为已迁移到 GFramework.Cqrs.Cqrs.Behaviors 命名空间, +/// 实现位于 GFramework.Cqrs 程序集,用于承载 CQRS runtime 细节并与旧层解耦。 /// public sealed class LoggingBehavior : IPipelineBehavior where TRequest : IRequest diff --git a/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs index 7d301009..f532f80d 100644 --- a/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -17,22 +17,24 @@ using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Cqrs.Cqrs.Query; /// -/// 抽象流式查询处理器基类 -/// 继承自轻量 CQRS 上下文基类并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 -/// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景 +/// 为流式查询处理器提供共享的 CQRS 上下文访问基类。 /// -/// 流式查询类型,必须实现IStreamQuery接口 -/// 流式查询响应元素类型 +/// 流式查询类型,必须实现 +/// 流式查询响应元素类型。 +/// +/// 该基类复用 的上下文注入能力,并实现 +/// 契约,让派生类只需聚焦于结果流的生成逻辑。 +/// 适用于需要逐步产出大量结果或长生命周期响应流的查询场景。 +/// public abstract class AbstractStreamQueryHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TQuery : IStreamQuery { /// - /// 处理流式查询并返回异步可枚举的响应序列 - /// 由具体的流式查询处理器子类实现流式查询处理逻辑 + /// 处理流式查询并返回异步可枚举的响应序列。 /// - /// 要处理的流式查询对象 - /// 取消令牌,用于取消流式查询操作 - /// 异步可枚举的响应序列,每个元素类型为TResponse + /// 要处理的流式查询对象。 + /// 用于停止结果流生成的取消令牌。 + /// 按需生成的异步响应序列。 public abstract IAsyncEnumerable Handle(TQuery query, CancellationToken cancellationToken); } diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs index 45dbb07a..0a0f86ce 100644 --- a/GFramework.Cqrs/CqrsRuntimeFactory.cs +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -21,6 +21,9 @@ public static class CqrsRuntimeFactory /// 目标依赖注入容器。 /// 用于 runtime 诊断的日志器。 /// 默认 CQRS runtime。 + /// + /// 。 + /// public static ICqrsRuntime CreateRuntime(IIocContainer container, ILogger logger) { ArgumentNullException.ThrowIfNull(container); @@ -35,6 +38,9 @@ public static class CqrsRuntimeFactory /// 目标依赖注入容器。 /// 用于注册阶段诊断的日志器。 /// 默认 CQRS handler registrar。 + /// + /// 。 + /// public static ICqrsHandlerRegistrar CreateHandlerRegistrar(IIocContainer container, ILogger logger) { ArgumentNullException.ThrowIfNull(container); diff --git a/GFramework.Cqrs/Notification/NotificationBase.cs b/GFramework.Cqrs/Notification/NotificationBase.cs index 06390406..93daea8f 100644 --- a/GFramework.Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Cqrs/Notification/NotificationBase.cs @@ -14,18 +14,22 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Notification; -namespace GFramework.Cqrs.Notification; +namespace GFramework.Core.Cqrs.Notification; /// -/// 表示一个基础通知类,用于处理带有输入的通知模式实现。 -/// 该类实现了 INotification 接口,提供了通用的通知结构。 +/// 为携带输入模型的 CQRS 通知提供统一基类。 /// -/// 通知输入数据的类型,必须实现 INotificationInput 接口 -/// 通知执行所需的输入数据 -public abstract class NotificationBase(TInput input) : INotification where TInput : INotificationInput +/// 通知输入类型,必须实现 +/// 通知广播时携带的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class NotificationBase(TInput input) : INotification + where TInput : INotificationInput { /// - /// 获取通知的输入数据。 + /// 获取通知广播时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Cqrs/Query/QueryBase.cs b/GFramework.Cqrs/Query/QueryBase.cs index d0f491b1..9d15e028 100644 --- a/GFramework.Cqrs/Query/QueryBase.cs +++ b/GFramework.Cqrs/Query/QueryBase.cs @@ -13,19 +13,23 @@ using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Cqrs.Query; +namespace GFramework.Core.Cqrs.Query; /// -/// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。 -/// 该类实现 IQuery<TResponse> 接口,提供了通用的查询结构。 +/// 为携带输入模型的 CQRS 查询提供统一基类。 /// -/// 查询输入数据的类型,必须实现 IQueryInput 接口 -/// 查询执行后返回结果的类型 -/// 查询执行所需的输入数据 -public abstract class QueryBase(TInput input) : IQuery where TInput : IQueryInput +/// 查询输入类型,必须实现 +/// 查询响应类型。 +/// 查询执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class QueryBase(TInput input) : IQuery + where TInput : IQueryInput { /// - /// 获取查询的输入数据。 + /// 获取查询执行时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Cqrs/Request/RequestBase.cs b/GFramework.Cqrs/Request/RequestBase.cs index 8c534c86..40d7a44d 100644 --- a/GFramework.Cqrs/Request/RequestBase.cs +++ b/GFramework.Cqrs/Request/RequestBase.cs @@ -14,19 +14,23 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Request; -namespace GFramework.Cqrs.Request; +namespace GFramework.Core.Cqrs.Request; /// -/// 表示一个基础请求类,用于处理带有输入和响应的请求模式实现。 -/// 该类实现了 IRequest<TResponse> 接口,提供了通用的请求结构。 +/// 为携带输入模型的通用 CQRS 请求提供统一基类。 /// -/// 请求输入数据的类型,必须实现 IRequestInput 接口 -/// 请求执行后返回结果的类型 -/// 请求执行所需的输入数据 -public abstract class RequestBase(TInput input) : IRequest where TInput : IRequestInput +/// 请求输入类型,必须实现 +/// 请求响应类型。 +/// 请求执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class RequestBase(TInput input) : IRequest + where TInput : IRequestInput { /// - /// 获取请求的输入数据。 + /// 获取请求执行时携带的输入对象。 /// public TInput Input => input; } diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index f044bbc8..ad02120e 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -4,11 +4,10 @@ using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Cqrs.Command; using GFramework.Core.Ioc; -using GFramework.Core.Logging; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; -using GFramework.Cqrs.Command; namespace GFramework.Tests.Common; @@ -48,7 +47,6 @@ public static class CqrsTestRuntime /// /// 目标测试容器。 /// - /// 反射调用底层 CQRS runtime 或注册器构造函数失败时抛出。 /// /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, /// 而无需完整启动服务模块管理器。 From 12c9c8a9ec24698792ab0cd5ed4304a0d4df00dc Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:27:55 +0800 Subject: [PATCH 43/70] =?UTF-8?q?refactor(cqrs):=20=E8=BF=81=E7=A7=BBCQRS?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=B1=BB=E5=9E=8B=E5=AE=9E=E7=8E=B0=E5=B9=B6?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E7=A8=8B=E5=BA=8F=E9=9B=86=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将CommandBase、QueryBase、RequestBase、NotificationBase类型从Core模块迁移到独立Cqrs模块 - 在GFramework.Core中添加TypeForwarder维持向后兼容性 - 创建CqrsPublicNamespaceCompatibilityTests验证运行时程序集转发功能 - 更新CqrsTestRuntime移除对已迁移类型的直接引用 - 为所有基础类型添加统一输入模型支持和命名空间兼容性注释 - 实现完整的CQRS抽象基类重构以支持模块化架构 --- .../Cqrs/CqrsPublicNamespaceCompatibilityTests.cs | 4 ---- GFramework.Core/Properties/TypeForwarders.cs | 4 ---- GFramework.Cqrs/Command/CommandBase.cs | 2 +- GFramework.Cqrs/Notification/NotificationBase.cs | 4 ++-- GFramework.Cqrs/Query/QueryBase.cs | 4 ++-- GFramework.Cqrs/Request/RequestBase.cs | 4 ++-- GFramework.Tests.Common/CqrsTestRuntime.cs | 1 - 7 files changed, 7 insertions(+), 16 deletions(-) diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs index 2f02dbb7..20470dd8 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -1,7 +1,3 @@ -using GFramework.Core.Cqrs.Command; -using GFramework.Core.Cqrs.Notification; -using GFramework.Core.Cqrs.Query; -using GFramework.Core.Cqrs.Request; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Command; using GFramework.Cqrs.Abstractions.Cqrs.Notification; diff --git a/GFramework.Core/Properties/TypeForwarders.cs b/GFramework.Core/Properties/TypeForwarders.cs index a27d2bf4..df43a698 100644 --- a/GFramework.Core/Properties/TypeForwarders.cs +++ b/GFramework.Core/Properties/TypeForwarders.cs @@ -1,9 +1,5 @@ using System.Runtime.CompilerServices; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Cqrs.Command; -using GFramework.Core.Cqrs.Notification; -using GFramework.Core.Cqrs.Query; -using GFramework.Core.Cqrs.Request; [assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))] [assembly: TypeForwardedTo(typeof(CommandBase<,>))] diff --git a/GFramework.Cqrs/Command/CommandBase.cs b/GFramework.Cqrs/Command/CommandBase.cs index 01351332..486e4136 100644 --- a/GFramework.Cqrs/Command/CommandBase.cs +++ b/GFramework.Cqrs/Command/CommandBase.cs @@ -13,7 +13,7 @@ using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Command; /// /// 为携带输入模型的 CQRS 命令提供统一基类。 diff --git a/GFramework.Cqrs/Notification/NotificationBase.cs b/GFramework.Cqrs/Notification/NotificationBase.cs index 93daea8f..b585a9a4 100644 --- a/GFramework.Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Cqrs/Notification/NotificationBase.cs @@ -14,12 +14,12 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Notification; -namespace GFramework.Core.Cqrs.Notification; +namespace GFramework.Cqrs.Notification; /// /// 为携带输入模型的 CQRS 通知提供统一基类。 /// -/// 通知输入类型,必须实现 +/// 通知输入类型,必须实现 /// 通知广播时携带的输入对象。 /// /// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 diff --git a/GFramework.Cqrs/Query/QueryBase.cs b/GFramework.Cqrs/Query/QueryBase.cs index 9d15e028..880abf0c 100644 --- a/GFramework.Cqrs/Query/QueryBase.cs +++ b/GFramework.Cqrs/Query/QueryBase.cs @@ -13,12 +13,12 @@ using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Query; /// /// 为携带输入模型的 CQRS 查询提供统一基类。 /// -/// 查询输入类型,必须实现 +/// 查询输入类型,必须实现 /// 查询响应类型。 /// 查询执行所需的输入对象。 /// diff --git a/GFramework.Cqrs/Request/RequestBase.cs b/GFramework.Cqrs/Request/RequestBase.cs index 40d7a44d..93af04ae 100644 --- a/GFramework.Cqrs/Request/RequestBase.cs +++ b/GFramework.Cqrs/Request/RequestBase.cs @@ -14,12 +14,12 @@ using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs.Request; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Request; /// /// 为携带输入模型的通用 CQRS 请求提供统一基类。 /// -/// 请求输入类型,必须实现 +/// 请求输入类型,必须实现 /// 请求响应类型。 /// 请求执行所需的输入对象。 /// diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index ad02120e..aec866df 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -4,7 +4,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Cqrs.Command; using GFramework.Core.Ioc; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; From b747787b8754d62c4dafcdf01103fa700039161b Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:36:55 +0800 Subject: [PATCH 44/70] =?UTF-8?q?refactor(core):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=BD=AC=E5=8F=91=E5=99=A8=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=87=E4=BB=B6=E7=9A=84=E5=91=BD=E5=90=8D=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 TypeForwarders.cs 中添加 Cqrs 相关命名空间引用 - 在 CqrsPublicNamespaceCompatibilityTests.cs 中同步更新命名空间导入 - 在 CqrsTestRuntime.cs 中补充 Command 命名空间引用 - 确保所有测试运行时环境的命名空间一致性 --- .../Cqrs/CqrsPublicNamespaceCompatibilityTests.cs | 4 ++++ GFramework.Core/Properties/TypeForwarders.cs | 4 ++++ GFramework.Tests.Common/CqrsTestRuntime.cs | 1 + 3 files changed, 9 insertions(+) diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs index 20470dd8..40506524 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -3,6 +3,10 @@ using GFramework.Cqrs.Abstractions.Cqrs.Command; using GFramework.Cqrs.Abstractions.Cqrs.Notification; using GFramework.Cqrs.Abstractions.Cqrs.Query; using GFramework.Cqrs.Abstractions.Cqrs.Request; +using GFramework.Cqrs.Command; +using GFramework.Cqrs.Notification; +using GFramework.Cqrs.Query; +using GFramework.Cqrs.Request; namespace GFramework.Core.Tests.Cqrs; diff --git a/GFramework.Core/Properties/TypeForwarders.cs b/GFramework.Core/Properties/TypeForwarders.cs index df43a698..2c260462 100644 --- a/GFramework.Core/Properties/TypeForwarders.cs +++ b/GFramework.Core/Properties/TypeForwarders.cs @@ -1,5 +1,9 @@ using System.Runtime.CompilerServices; using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Command; +using GFramework.Cqrs.Notification; +using GFramework.Cqrs.Query; +using GFramework.Cqrs.Request; [assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))] [assembly: TypeForwardedTo(typeof(CommandBase<,>))] diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index aec866df..e7c971db 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -7,6 +7,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Command; namespace GFramework.Tests.Common; From 7e402d91d3b118e81e8f79321044472edeb563ca Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:50:21 +0800 Subject: [PATCH 45/70] =?UTF-8?q?docs(cqrs):=20=E4=BF=AE=E6=AD=A3=E6=B3=9B?= =?UTF-8?q?=E5=9E=8B=E7=B1=BB=E5=9E=8B=E5=8F=82=E6=95=B0=E7=9A=84XML?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正NotificationBase中TInput类型的XML文档注释,将input更正为INotificationInput - 修正QueryBase中TInput类型的XML文档注释,将TResponse更正为IQueryInput - 修正RequestBase中TInput类型的XML文档注释,将TResponse更正为IRequestInput --- GFramework.Cqrs/Notification/NotificationBase.cs | 2 +- GFramework.Cqrs/Query/QueryBase.cs | 2 +- GFramework.Cqrs/Request/RequestBase.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GFramework.Cqrs/Notification/NotificationBase.cs b/GFramework.Cqrs/Notification/NotificationBase.cs index b585a9a4..653a22f2 100644 --- a/GFramework.Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Cqrs/Notification/NotificationBase.cs @@ -19,7 +19,7 @@ namespace GFramework.Cqrs.Notification; /// /// 为携带输入模型的 CQRS 通知提供统一基类。 /// -/// 通知输入类型,必须实现 +/// 通知输入类型,必须实现 /// 通知广播时携带的输入对象。 /// /// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 diff --git a/GFramework.Cqrs/Query/QueryBase.cs b/GFramework.Cqrs/Query/QueryBase.cs index 880abf0c..4da82bf3 100644 --- a/GFramework.Cqrs/Query/QueryBase.cs +++ b/GFramework.Cqrs/Query/QueryBase.cs @@ -18,7 +18,7 @@ namespace GFramework.Cqrs.Query; /// /// 为携带输入模型的 CQRS 查询提供统一基类。 /// -/// 查询输入类型,必须实现 +/// 查询输入类型,必须实现 /// 查询响应类型。 /// 查询执行所需的输入对象。 /// diff --git a/GFramework.Cqrs/Request/RequestBase.cs b/GFramework.Cqrs/Request/RequestBase.cs index 93af04ae..08ceabe4 100644 --- a/GFramework.Cqrs/Request/RequestBase.cs +++ b/GFramework.Cqrs/Request/RequestBase.cs @@ -19,7 +19,7 @@ namespace GFramework.Cqrs.Request; /// /// 为携带输入模型的通用 CQRS 请求提供统一基类。 /// -/// 请求输入类型,必须实现 +/// 请求输入类型,必须实现 /// 请求响应类型。 /// 请求执行所需的输入对象。 /// From 7a2127b50e7414865bdafbc755a5d3c0bab9b539 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:51:10 +0800 Subject: [PATCH 46/70] =?UTF-8?q?refactor(cqrs):=20=E6=9B=B4=E6=96=B0Cqrs?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将GFramework.Core.Cqrs.Command更改为GFramework.Cqrs.Command - 将GFramework.Core.Cqrs.Query更改为GFramework.Cqrs.Query - 将GFramework.Core.Cqrs.Request更改为GFramework.Cqrs.Request - 将GFramework.Core.Cqrs.Notification更改为GFramework.Cqrs.Notification --- .../Cqrs/CqrsPublicNamespaceCompatibilityTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs index 40506524..dd119413 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -24,10 +24,10 @@ public sealed class CqrsPublicNamespaceCompatibilityTests { Assert.Multiple(() => { - AssertLegacyType(typeof(CommandBase), "GFramework.Core.Cqrs.Command"); - AssertLegacyType(typeof(QueryBase), "GFramework.Core.Cqrs.Query"); - AssertLegacyType(typeof(RequestBase), "GFramework.Core.Cqrs.Request"); - AssertLegacyType(typeof(NotificationBase), "GFramework.Core.Cqrs.Notification"); + AssertLegacyType(typeof(CommandBase), "GFramework.Cqrs.Command"); + AssertLegacyType(typeof(QueryBase), "GFramework.Cqrs.Query"); + AssertLegacyType(typeof(RequestBase), "GFramework.Cqrs.Request"); + AssertLegacyType(typeof(NotificationBase), "GFramework.Cqrs.Notification"); }); } From ff9b01063920f3e27c26e84b7d0c1825eb99084e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:51:31 +0800 Subject: [PATCH 47/70] =?UTF-8?q?refactor(cqrs):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B=E7=9A=84?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=92=8C=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E9=9B=86=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将基础消息类型从遗留的Core.Cqrs命名空间迁移到新的Cqrs命名空间 - 保持运行时程序集承载实现的架构设计 - 更新测试方法名称以反映新的命名空间结构 - 确保公共API兼容性的同时优化组件组织结构 --- .../Cqrs/CqrsPublicNamespaceCompatibilityTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs index dd119413..dd99d8dc 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -20,7 +20,7 @@ public sealed class CqrsPublicNamespaceCompatibilityTests /// 验证基础消息类型继续暴露在历史 Core.Cqrs 命名空间,同时由独立 runtime 程序集承载实现。 /// [Test] - public void Base_Message_Types_Should_Remain_In_Legacy_Namespaces_While_Living_In_Runtime_Assembly() + public void Base_Message_Types_Should_Live_In_Cqrs_Namespaces_And_Runtime_Assembly() { Assert.Multiple(() => { From 7b63a65f51196200118f68f18bcd9795bd166080 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:52:26 +0800 Subject: [PATCH 48/70] =?UTF-8?q?refactor(tests):=20=E9=87=8D=E6=9E=84CQRS?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=BD=AC=E5=8F=91=E6=B5=8B=E8=AF=95=E4=BB=A5?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B0=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新测试方法名称从 GFramework_Core_Assembly_Should_Forward_Legacy_Base_Types_To_Runtime_Assembly 为 Type_Forwarding_Should_Resolve_Cqrs_Types_From_Core_Assembly - 将断言中的程序集限定名从 GFramework.Core.Cqrs.* 更新为 GFramework.Cqrs.* - 保持对命令、查询、请求和通知基类型的验证逻辑不变 --- .../Cqrs/CqrsPublicNamespaceCompatibilityTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs index dd99d8dc..f89ccddc 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -35,14 +35,14 @@ public sealed class CqrsPublicNamespaceCompatibilityTests /// 验证旧的 GFramework.Core 程序集限定名仍可解析到迁移后的 runtime 实现类型。 /// [Test] - public void GFramework_Core_Assembly_Should_Forward_Legacy_Base_Types_To_Runtime_Assembly() + public void Type_Forwarding_Should_Resolve_Cqrs_Types_From_Core_Assembly() { Assert.Multiple(() => { - AssertForwardedType("GFramework.Core.Cqrs.Command.CommandBase`2, GFramework.Core"); - AssertForwardedType("GFramework.Core.Cqrs.Query.QueryBase`2, GFramework.Core"); - AssertForwardedType("GFramework.Core.Cqrs.Request.RequestBase`2, GFramework.Core"); - AssertForwardedType("GFramework.Core.Cqrs.Notification.NotificationBase`1, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Command.CommandBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Query.QueryBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Request.RequestBase`2, GFramework.Core"); + AssertForwardedType("GFramework.Cqrs.Notification.NotificationBase`1, GFramework.Core"); }); } From 922ad43b5e0728259b9523c7d67fe09c4dc7fa31 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:59:46 +0800 Subject: [PATCH 49/70] =?UTF-8?q?fix(cqrs):=20=E4=BF=AE=E5=A4=8DCQRS?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=85=BC=E5=AE=B9=E6=80=A7?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=B8=AD=E7=9A=84=E7=B1=BB=E5=9E=8B=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新注释以更准确描述基础消息类型的命名空间暴露方式 - 将Type.GetType的throwOnError参数从true改为false以避免异常抛出 - 调整测试逻辑以更好地处理类型解析场景 --- .../Cqrs/CqrsPublicNamespaceCompatibilityTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs index f89ccddc..64f69f02 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -17,7 +17,7 @@ namespace GFramework.Core.Tests.Cqrs; public sealed class CqrsPublicNamespaceCompatibilityTests { /// - /// 验证基础消息类型继续暴露在历史 Core.Cqrs 命名空间,同时由独立 runtime 程序集承载实现。 + /// 验证基础消息类型继续暴露在历史公开 CQRS 命名空间(GFramework.Cqrs.*),同时由独立 runtime 程序集承载实现。 /// [Test] public void Base_Message_Types_Should_Live_In_Cqrs_Namespaces_And_Runtime_Assembly() @@ -57,7 +57,7 @@ public sealed class CqrsPublicNamespaceCompatibilityTests private static void AssertForwardedType(string assemblyQualifiedTypeName) { - var resolvedType = Type.GetType(assemblyQualifiedTypeName, throwOnError: true); + var resolvedType = Type.GetType(assemblyQualifiedTypeName, throwOnError: false); Assert.Multiple(() => { From 1973fb2a60156a0d95c6f53f90d472e9dd528172 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:32:17 +0800 Subject: [PATCH 50/70] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0Microsoft?= =?UTF-8?q?=20DI=E5=AE=B9=E5=99=A8=E9=80=82=E9=85=8D=E5=99=A8=E5=92=8CCQRS?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除过时的Cqrs抽象引用 - 添加MicrosoftDiContainer实现IIocContainer接口 - 提供线程安全的依赖注入容器功能 - 支持单例、瞬态、作用域生命周期管理 - 实现CQRS请求管道行为注册功能 - 添加CqrsRuntimeModule服务模块 - 提供CQRS运行时实现和处理器注册器 - 扩展IArchitectureContext接口支持CQRS契约 --- .../Architectures/IArchitectureContext.cs | 2 +- .../Cqrs/ICqrsRuntime.cs | 54 ++++--------------- .../Architectures/ArchitectureContextTests.cs | 1 - .../Ioc/MicrosoftDiContainerTests.cs | 6 ++- .../Architectures/ArchitectureContext.cs | 10 ++-- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 11 ++-- .../Services/Modules/CqrsRuntimeModule.cs | 6 ++- .../Cqrs/ICqrsContext.cs | 13 +++++ .../Cqrs/ICqrsRuntime.cs | 49 +++++++++++++++++ GFramework.Cqrs/CqrsRuntimeFactory.cs | 1 - GFramework.Cqrs/Internal/CqrsDispatcher.cs | 30 ++++++----- GFramework.Tests.Common/CqrsTestRuntime.cs | 9 +++- 12 files changed, 118 insertions(+), 74 deletions(-) create mode 100644 GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs create mode 100644 GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index c7b62e2f..2e8894bc 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -18,7 +18,7 @@ namespace GFramework.Core.Abstractions.Architectures; /// 新的 GFramework.Cqrs.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 /// 新功能优先使用 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。 /// -public interface IArchitectureContext +public interface IArchitectureContext : ICqrsContext { /// /// 获取指定类型的服务实例 diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs index d9efcb07..5bcbf862 100644 --- a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs @@ -1,52 +1,16 @@ -using GFramework.Core.Abstractions.Architectures; -using GFramework.Cqrs.Abstractions.Cqrs; +using System.ComponentModel; namespace GFramework.Core.Abstractions.Cqrs; /// -/// 定义架构上下文使用的 CQRS runtime seam。 -/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦, -/// 使 不再直接依赖某个固定的 runtime 类型。 +/// 提供旧 GFramework.Core.Abstractions.Cqrs 命名空间下的 CQRS runtime 兼容别名。 /// -public interface ICqrsRuntime +/// +/// 正式 runtime seam 已迁移到 , +/// 但当前仍保留该接口以避免立即打断历史公开路径与既有二进制引用。 +/// 新代码应优先依赖 GFramework.Cqrs.Abstractions.Cqrs 下的正式契约。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface ICqrsRuntime : GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime { - /// - /// 发送请求并返回响应。 - /// - /// 响应类型。 - /// 当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。 - /// 要分发的请求。 - /// 取消令牌。 - /// 请求响应。 - ValueTask SendAsync( - IArchitectureContext context, - IRequest request, - CancellationToken cancellationToken = default); - - /// - /// 发布通知到所有已注册处理器。 - /// - /// 通知类型。 - /// 当前架构上下文,用于上下文感知处理器注入。 - /// 要发布的通知。 - /// 取消令牌。 - /// 表示通知分发完成的值任务。 - ValueTask PublishAsync( - IArchitectureContext context, - TNotification notification, - CancellationToken cancellationToken = default) - where TNotification : INotification; - - /// - /// 创建流式请求的异步响应序列。 - /// - /// 流元素类型。 - /// 当前架构上下文,用于上下文感知处理器注入。 - /// 流式请求。 - /// 取消令牌。 - /// 按需生成的异步响应序列。 - IAsyncEnumerable CreateStream( - IArchitectureContext context, - IStreamRequest request, - CancellationToken cancellationToken = default); } diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index cdcde44d..584090ab 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -1,7 +1,6 @@ using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Ioc; diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 4104ccb1..39445d48 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,12 +1,12 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; using GFramework.Cqrs.Abstractions.Cqrs; +using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Core.Tests.Ioc; @@ -160,12 +160,16 @@ public class MicrosoftDiContainerTests public void RegisterHandlers_Should_Not_Duplicate_Cqrs_Infrastructure_When_It_Is_Already_Registered() { Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.Get(), Is.SameAs(_container.Get())); CqrsTestRuntime.RegisterHandlers(_container); Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.Get(), Is.SameAs(_container.Get())); } /// diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 9b6d7dc2..e0ac2dd6 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -190,7 +189,7 @@ public class ArchitectureContext : IArchitectureContext /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(Cqrs.Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -216,8 +215,7 @@ public class ArchitectureContext : IArchitectureContext /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync( - GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, + public async ValueTask SendQueryAsync(Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -354,7 +352,7 @@ public class ArchitectureContext : IArchitectureContext /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask public async ValueTask SendCommandAsync( - GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, + Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -393,7 +391,7 @@ public class ArchitectureContext : IArchitectureContext /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(Cqrs.Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index dc14485e..390f4c91 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -4,7 +4,6 @@ using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; -using GFramework.Core.Logging; using GFramework.Core.Rule; using GFramework.Cqrs.Abstractions.Cqrs; @@ -624,11 +623,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) .Where(s => s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType)).ToList(); var result = new List(); + var seenInstances = new HashSet(ReferenceEqualityComparer.Instance); foreach (var descriptor in registeredServices) { if (descriptor.ImplementationInstance is T instance) { - result.Add(instance); + // 同一实例可能同时以“正式接口 + 兼容别名接口”被注册;未冻结路径需去重以保持与冻结后的解析口径一致。 + if (seenInstances.Add(instance)) + result.Add(instance); } else if (descriptor.ImplementationFactory != null) { @@ -672,11 +674,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) .ToList(); var result = new List(); + var seenInstances = new HashSet(ReferenceEqualityComparer.Instance); foreach (var descriptor in registeredServices) { if (descriptor.ImplementationInstance != null) { - result.Add(descriptor.ImplementationInstance); + // 同一实例可能通过多个可赋值服务类型暴露;返回前按引用去重,避免兼容别名造成重复观察结果。 + if (seenInstances.Add(descriptor.ImplementationInstance)) + result.Add(descriptor.ImplementationInstance); } else if (descriptor.ImplementationFactory != null) { diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 3fe37558..9a36d003 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -1,9 +1,9 @@ using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; +using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Core.Services.Modules; @@ -39,8 +39,10 @@ public sealed class CqrsRuntimeModule : IServiceModule var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); + var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger); - container.Register(CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger)); + container.Register(runtime); + container.Register((LegacyICqrsRuntime)runtime); container.Register( CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger)); } diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs new file mode 100644 index 00000000..09fc355e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs @@ -0,0 +1,13 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义 CQRS runtime 在分发期间携带的最小上下文标记。 +/// +/// +/// 该接口当前刻意保持为轻量 marker seam,只用于让 从 +/// GFramework.Core.AbstractionsIArchitectureContext 解耦。 +/// 运行时实现仍可在需要时识别更具体的上下文类型,并对现有 IContextAware 处理器执行兼容注入。 +/// +public interface ICqrsContext +{ +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs new file mode 100644 index 00000000..b8c85124 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs @@ -0,0 +1,49 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义架构上下文使用的 CQRS runtime seam。 +/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦, +/// 使 CQRS runtime 契约可独立归属到 GFramework.Cqrs.Abstractions。 +/// +public interface ICqrsRuntime +{ + /// + /// 发送请求并返回响应。 + /// + /// 响应类型。 + /// 当前 CQRS 分发上下文。 + /// 要分发的请求。 + /// 取消令牌。 + /// 请求响应。 + ValueTask SendAsync( + ICqrsContext context, + IRequest request, + CancellationToken cancellationToken = default); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前 CQRS 分发上下文。 + /// 要发布的通知。 + /// 取消令牌。 + /// 表示通知分发完成的值任务。 + ValueTask PublishAsync( + ICqrsContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification; + + /// + /// 创建流式请求的异步响应序列。 + /// + /// 流元素类型。 + /// 当前 CQRS 分发上下文。 + /// 流式请求。 + /// 取消令牌。 + /// 按需生成的异步响应序列。 + IAsyncEnumerable CreateStream( + ICqrsContext context, + IStreamRequest request, + CancellationToken cancellationToken = default); +} diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs index 0a0f86ce..1357975d 100644 --- a/GFramework.Cqrs/CqrsRuntimeFactory.cs +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -1,4 +1,3 @@ -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index dafea402..9a125789 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -1,17 +1,17 @@ using System.Collections.Concurrent; using System.Reflection; using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Rule; using GFramework.Cqrs.Abstractions.Cqrs; +using ICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Cqrs.Internal; /// /// GFramework 自有 CQRS 运行时分发器。 -/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前架构上下文。 +/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前 CQRS 分发上下文。 /// internal sealed class CqrsDispatcher( IIocContainer container, @@ -38,11 +38,11 @@ internal sealed class CqrsDispatcher( /// 发布通知到所有已注册处理器。 /// /// 通知类型。 - /// 当前架构上下文,用于上下文感知处理器注入。 + /// 当前 CQRS 分发上下文,用于上下文感知处理器注入。 /// 通知对象。 /// 取消令牌。 public async ValueTask PublishAsync( - IArchitectureContext context, + ICqrsContext context, TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification @@ -75,12 +75,12 @@ internal sealed class CqrsDispatcher( /// 发送请求并返回结果。 /// /// 响应类型。 - /// 当前架构上下文,用于上下文感知处理器注入。 + /// 当前 CQRS 分发上下文,用于上下文感知处理器注入。 /// 请求对象。 /// 取消令牌。 /// 请求响应。 public async ValueTask SendAsync( - IArchitectureContext context, + ICqrsContext context, IRequest request, CancellationToken cancellationToken = default) { @@ -122,12 +122,12 @@ internal sealed class CqrsDispatcher( /// 创建流式请求并返回异步响应序列。 /// /// 响应元素类型。 - /// 当前架构上下文,用于上下文感知处理器注入。 + /// 当前 CQRS 分发上下文,用于上下文感知处理器注入。 /// 流式请求对象。 /// 取消令牌。 /// 异步响应序列。 public IAsyncEnumerable CreateStream( - IArchitectureContext context, + ICqrsContext context, IStreamRequest request, CancellationToken cancellationToken = default) { @@ -150,14 +150,20 @@ internal sealed class CqrsDispatcher( } /// - /// 为上下文感知处理器注入当前架构上下文。 + /// 为上下文感知处理器注入当前 CQRS 分发上下文。 /// /// 处理器实例。 - /// 当前架构上下文。 - private static void PrepareHandler(object handler, IArchitectureContext context) + /// 当前 CQRS 分发上下文。 + private static void PrepareHandler(object handler, ICqrsContext context) { if (handler is IContextAware contextAware) - contextAware.SetContext(context); + { + if (context is not IArchitectureContext architectureContext) + throw new InvalidOperationException( + "The current CQRS context does not implement IArchitectureContext, so it cannot be injected into IContextAware handlers."); + + contextAware.SetContext(architectureContext); + } } /// diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index e7c971db..7acbbe35 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Command; +using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; namespace GFramework.Tests.Common; @@ -60,7 +60,12 @@ public static class CqrsTestRuntime { var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger); - container.Register(runtime); + container.Register(runtime); + container.Register((LegacyICqrsRuntime)runtime); + } + else if (container.Get() is null) + { + container.Register((LegacyICqrsRuntime)container.GetRequired()); } if (container.Get() is null) From a7604de804a25e9e4b6c29bf8f5c64e01ef8c61e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:37:40 +0800 Subject: [PATCH 51/70] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0=20Microso?= =?UTF-8?q?ft=20DI=20=E5=AE=B9=E5=99=A8=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E5=92=8C=20CQRS=20=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 MicrosoftDiContainer 类,提供对 Microsoft.Extensions.DependencyInjection 的适配 - 添加线程安全的依赖注入容器功能,支持单例、瞬态和作用域服务注册 - 实现 CqrsRuntimeModule 模块,用于注册 CQRS 运行时组件 - 添加 CqrsRuntimeFactory 工厂类,提供 CQRS 运行时实现的创建入口 - 实现 DefaultCqrsRegistrationService,处理 CQRS 处理器的程序集注册 - 添加 CqrsTestRuntime 测试工具类,为测试环境提供 CQRS 运行时访问 - 支持多种注册方式包括实例注册、类型映射和工厂方法 - 实现服务获取、查询和生命周期管理功能 - 添加容器冻结机制以构建服务提供者 - 支持 CQRS 管道行为和处理器的批量注册功能 --- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 56 ++++-------------- .../Services/Modules/CqrsRuntimeModule.cs | 7 ++- GFramework.Cqrs/CqrsRuntimeFactory.cs | 17 ++++++ GFramework.Cqrs/ICqrsRegistrationService.cs | 19 ++++++ .../DefaultCqrsRegistrationService.cs | 59 +++++++++++++++++++ GFramework.Tests.Common/CqrsTestRuntime.cs | 8 +++ 6 files changed, 118 insertions(+), 48 deletions(-) create mode 100644 GFramework.Cqrs/ICqrsRegistrationService.cs create mode 100644 GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 390f4c91..78295ed4 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -5,6 +5,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Rule; +using GFramework.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Ioc; @@ -56,12 +57,6 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// private readonly HashSet _registeredInstances = []; - /// - /// 已接入 CQRS handler 注册流程的程序集键集合。 - /// 使用稳定字符串键而不是 Assembly 引用本身,以避免默认路径和显式扩展路径使用不同 Assembly 对象时重复注册。 - /// - private readonly HashSet _registeredCqrsHandlerAssemblyKeys = new(StringComparer.Ordinal); - /// /// 日志记录器,用于记录容器操作日志 /// @@ -405,26 +400,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) try { ThrowIfFrozen(); - - var processedAssemblyKeys = new HashSet(StringComparer.Ordinal); - foreach (var assembly in assemblies - .Where(static assembly => assembly is not null) - .OrderBy(GetCqrsAssemblyRegistrationKey, StringComparer.Ordinal)) - { - var assemblyKey = GetCqrsAssemblyRegistrationKey(assembly); - if (!processedAssemblyKeys.Add(assemblyKey)) - continue; - - if (_registeredCqrsHandlerAssemblyKeys.Contains(assemblyKey)) - { - _logger.Debug( - $"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered."); - continue; - } - - ResolveCqrsHandlerRegistrar().RegisterHandlers([assembly]); - _registeredCqrsHandlerAssemblyKeys.Add(assemblyKey); - } + ResolveCqrsRegistrationService().RegisterHandlers(assemblies); } finally { @@ -455,22 +431,22 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) #region Get /// - /// 获取当前容器中已注册的 CQRS 处理器注册器。 + /// 获取当前容器中已注册的 CQRS 程序集注册协调器。 /// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定, /// 避免在容器未冻结前依赖完整的服务提供者构建流程。 /// - /// 已注册的 CQRS 处理器注册器实例。 - /// 未找到可用的 CQRS 处理器注册器实例时抛出。 - private ICqrsHandlerRegistrar ResolveCqrsHandlerRegistrar() + /// 已注册的 CQRS 程序集注册协调器实例。 + /// 未找到可用的 CQRS 程序集注册协调器实例时抛出。 + private ICqrsRegistrationService ResolveCqrsRegistrationService() { var descriptor = GetServicesUnsafe.LastOrDefault(static service => - service.ServiceType == typeof(ICqrsHandlerRegistrar)); + service.ServiceType == typeof(ICqrsRegistrationService)); - if (descriptor?.ImplementationInstance is ICqrsHandlerRegistrar registrar) - return registrar; + if (descriptor?.ImplementationInstance is ICqrsRegistrationService registrationService) + return registrationService; const string errorMessage = - "ICqrsHandlerRegistrar not registered. Ensure the CQRS runtime module has been installed before registering handlers."; + "ICqrsRegistrationService not registered. Ensure the CQRS runtime module has been installed before registering handlers."; _logger.Error(errorMessage); throw new InvalidOperationException(errorMessage); } @@ -832,7 +808,6 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) GetServicesUnsafe.Clear(); _registeredInstances.Clear(); - _registeredCqrsHandlerAssemblyKeys.Clear(); _provider = null; _logger.Info("Container cleared"); } @@ -904,16 +879,5 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } - /// - /// 生成 CQRS handler 注册用的稳定程序集键。 - /// 该键需要同时兼顾真实程序集与测试中使用的 mocked Assembly,避免仅靠引用比较导致重复接入。 - /// - /// 目标程序集。 - /// 稳定的程序集标识字符串。 - private static string GetCqrsAssemblyRegistrationKey(Assembly assembly) - { - return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString(); - } - #endregion } diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs index 9a36d003..1da8f684 100644 --- a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -39,12 +39,15 @@ 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 registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); container.Register(runtime); container.Register((LegacyICqrsRuntime)runtime); - container.Register( - CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger)); + container.Register(registrar); + container.Register( + CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger)); } /// diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs index 1357975d..cbed68aa 100644 --- a/GFramework.Cqrs/CqrsRuntimeFactory.cs +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -47,4 +47,21 @@ public static class CqrsRuntimeFactory return new DefaultCqrsHandlerRegistrar(container, logger); } + + /// + /// 创建默认的 CQRS 程序集注册协调器。 + /// + /// 底层 handler 注册器。 + /// 用于注册阶段诊断的日志器。 + /// 默认 CQRS 程序集注册协调器。 + /// + /// 。 + /// + public static ICqrsRegistrationService CreateRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger) + { + ArgumentNullException.ThrowIfNull(registrar); + ArgumentNullException.ThrowIfNull(logger); + + return new DefaultCqrsRegistrationService(registrar, logger); + } } diff --git a/GFramework.Cqrs/ICqrsRegistrationService.cs b/GFramework.Cqrs/ICqrsRegistrationService.cs new file mode 100644 index 00000000..89a0a9d1 --- /dev/null +++ b/GFramework.Cqrs/ICqrsRegistrationService.cs @@ -0,0 +1,19 @@ +using System.Reflection; + +namespace GFramework.Cqrs; + +/// +/// 协调 CQRS 处理器程序集的接入流程。 +/// +/// +/// 该服务封装“程序集去重 + 生成注册器优先 + 反射回退”的默认接入语义, +/// 让 GFramework.Core 的容器层只保留公开入口,而不再直接维护 CQRS handler 注册细节。 +/// +public interface ICqrsRegistrationService +{ + /// + /// 注册一个或多个程序集中的 CQRS 处理器。 + /// + /// 要接入的程序集集合。 + void RegisterHandlers(IEnumerable assemblies); +} diff --git a/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs b/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs new file mode 100644 index 00000000..712ebea6 --- /dev/null +++ b/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Internal; + +/// +/// 默认的 CQRS 程序集注册协调器。 +/// +/// +/// 该实现把“按稳定程序集键去重”和“委托给 handler registrar 执行实际映射注册”收敛到 CQRS runtime 内部, +/// 避免外层容器继续了解 handler 注册流水线的内部结构。 +/// +internal sealed class DefaultCqrsRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger) + : ICqrsRegistrationService +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly HashSet _registeredAssemblyKeys = new(StringComparer.Ordinal); + private readonly ICqrsHandlerRegistrar _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); + + /// + /// 注册指定程序集中的 CQRS handlers。 + /// + /// 要接入的程序集集合。 + public void RegisterHandlers(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + + var processedAssemblyKeys = new HashSet(StringComparer.Ordinal); + foreach (var assembly in assemblies + .Where(static assembly => assembly is not null) + .OrderBy(GetAssemblyRegistrationKey, StringComparer.Ordinal)) + { + var assemblyKey = GetAssemblyRegistrationKey(assembly); + if (!processedAssemblyKeys.Add(assemblyKey)) + continue; + + if (_registeredAssemblyKeys.Contains(assemblyKey)) + { + _logger.Debug( + $"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered."); + continue; + } + + _registrar.RegisterHandlers([assembly]); + _registeredAssemblyKeys.Add(assemblyKey); + } + } + + /// + /// 生成稳定程序集键,避免相同程序集被不同 实例重复接入。 + /// + /// 目标程序集。 + /// 稳定的程序集标识。 + private static string GetAssemblyRegistrationKey(Assembly assembly) + { + return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString(); + } +} diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs index 7acbbe35..6109bd54 100644 --- a/GFramework.Tests.Common/CqrsTestRuntime.cs +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -74,6 +74,14 @@ public static class CqrsTestRuntime var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); container.Register(registrar); } + + if (container.Get() is null) + { + var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService"); + var registrar = container.GetRequired(); + var registrationService = CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger); + container.Register(registrationService); + } } /// From bc9336428ed6e273312b3aaa810c8b4d260cb76a Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:49:13 +0800 Subject: [PATCH 52/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8?= =?UTF-8?q?=E5=92=8C=E6=BA=90=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsHandlerRegistrar 类用于扫描并注册 CQRS 处理器 - 添加源码生成器自动生成 CQRS 处理器注册器减少反射开销 - 实现运行时回退机制在生成注册器不可用时使用反射扫描 - 添加完整的单元测试验证处理器注册顺序和容错行为 - 支持请求、通知和流式处理器的自动注册功能 - 实现稳定的处理器注册顺序保证跨环境一致性 - 添加详细的诊断日志记录注册过程和异常情况 --- .../Cqrs/CqrsHandlerRegistrarTests.cs | 99 ++++++++++++++- .../CqrsReflectionFallbackAttribute.cs | 14 +++ .../Internal/CqrsHandlerRegistrar.cs | 66 ++++++++-- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 119 +++++++++++++++++- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 65 +++++++--- 5 files changed, 328 insertions(+), 35 deletions(-) create mode 100644 GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 3100ce84..318f6d21 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -13,6 +13,9 @@ namespace GFramework.Cqrs.Tests.Cqrs; [TestFixture] internal sealed class CqrsHandlerRegistrarTests { + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// /// 初始化测试容器并重置共享状态。 /// @@ -42,9 +45,6 @@ internal sealed class CqrsHandlerRegistrarTests DeterministicNotificationHandlerState.Reset(); } - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。 /// @@ -188,6 +188,50 @@ internal sealed class CqrsHandlerRegistrarTests LoggerFactoryResolver.Provider = originalProvider; } } + + /// + /// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers, + /// 同时避免把已由生成注册器注册的映射重复写入服务集合。 + /// + [Test] + public void RegisterHandlers_Should_Combine_Generated_Registry_With_Reflection_Fallback_Without_Duplicates() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.PartialGeneratedRegistryAssembly, Version=1.0.0.0"); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) + .Returns([new CqrsReflectionFallbackAttribute()]); + generatedAssembly + .Setup(static assembly => assembly.GetTypes()) + .Returns( + [ + typeof(GeneratedRegistryNotificationHandler), + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + ]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var registrations = container.GetServicesUnsafe + .Where(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler) && + descriptor.ImplementationType is not null) + .Select(static descriptor => descriptor.ImplementationType!) + .ToList(); + + Assert.That( + registrations, + Is.EqualTo( + [ + typeof(GeneratedRegistryNotificationHandler), + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + ])); + } } /// @@ -337,3 +381,52 @@ internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistr $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); } } + +/// +/// 用于验证“生成注册器 + reflection fallback”组合路径的私有嵌套处理器容器。 +/// +internal sealed class ReflectionFallbackNotificationContainer +{ + /// + /// 获取仅能通过反射补扫接入的私有嵌套处理器类型。 + /// + public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler); + + private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler + : INotificationHandler + { + /// + /// 处理测试通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + } +} + +/// +/// 模拟局部生成注册器场景中,仅注册“可由生成代码直接引用”的那部分 handlers。 +/// +internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将生成路径可见的通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs new file mode 100644 index 00000000..f18a7344 --- /dev/null +++ b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs @@ -0,0 +1,14 @@ +namespace GFramework.Cqrs; + +/// +/// 标记程序集中的 CQRS 生成注册器仍需要运行时补充反射扫描。 +/// +/// +/// 该特性通常由源码生成器自动添加到消费端程序集。 +/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描, +/// 以覆盖那些生成代码无法直接引用的 handler 类型。 +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class CqrsReflectionFallbackAttribute : Attribute +{ +} diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 85a95509..435c9cd5 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -1,4 +1,3 @@ -using System.Reflection; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; @@ -32,7 +31,9 @@ internal static class CqrsHandlerRegistrar .Distinct() .OrderBy(GetAssemblySortKey, StringComparer.Ordinal)) { - if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger)) + var generatedRegistrationResult = + TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger); + if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled) continue; RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger); @@ -45,8 +46,11 @@ internal static class CqrsHandlerRegistrar /// 目标服务集合。 /// 当前要处理的程序集。 /// 日志记录器。 - /// 当成功使用生成注册器时返回 ;否则返回 - private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger) + /// 生成注册器的使用结果。 + private static GeneratedRegistrationResult TryRegisterGeneratedHandlers( + IServiceCollection services, + Assembly assembly, + ILogger logger) { var assemblyName = GetAssemblySortKey(assembly); @@ -62,7 +66,7 @@ internal static class CqrsHandlerRegistrar .ToList(); if (registryTypes.Count == 0) - return false; + return GeneratedRegistrationResult.NoGeneratedRegistry; var registries = new List(registryTypes.Count); foreach (var registryType in registryTypes) @@ -71,21 +75,21 @@ internal static class CqrsHandlerRegistrar { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); - return false; + return GeneratedRegistrationResult.NoGeneratedRegistry; } if (registryType.IsAbstract) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); - return false; + return GeneratedRegistrationResult.NoGeneratedRegistry; } if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated."); - return false; + return GeneratedRegistrationResult.NoGeneratedRegistry; } registries.Add(registry); @@ -98,7 +102,14 @@ internal static class CqrsHandlerRegistrar registry.Register(services, logger); } - return true; + if (RequiresReflectionFallback(assembly)) + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested reflection fallback for unsupported handlers."); + return GeneratedRegistrationResult.RequiresReflectionFallback; + } + + return GeneratedRegistrationResult.FullyHandled; } catch (Exception exception) { @@ -106,7 +117,7 @@ internal static class CqrsHandlerRegistrar $"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan."); logger.Warn( $"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}"); - return false; + return GeneratedRegistrationResult.NoGeneratedRegistry; } } @@ -128,6 +139,13 @@ internal static class CqrsHandlerRegistrar foreach (var handlerInterface in handlerInterfaces) { + if (IsHandlerMappingAlreadyRegistered(services, handlerInterface, implementationType)) + { + logger.Debug( + $"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); + continue; + } + // Request/notification handlers receive context injection before every dispatch. // Transient registration avoids sharing mutable Context across concurrent requests. services.AddTransient(handlerInterface, implementationType); @@ -202,6 +220,27 @@ internal static class CqrsHandlerRegistrar definition == typeof(IStreamRequestHandler<,>); } + /// + /// 判断生成注册器是否要求运行时继续补充反射扫描。 + /// + private static bool RequiresReflectionFallback(Assembly assembly) + { + return assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)?.Length > 0; + } + + /// + /// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。 + /// + private static bool IsHandlerMappingAlreadyRegistered( + IServiceCollection services, + Type handlerInterface, + Type implementationType) + { + return services.Any(descriptor => + descriptor.ServiceType == handlerInterface && + descriptor.ImplementationType == implementationType); + } + /// /// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。 /// @@ -217,4 +256,11 @@ internal static class CqrsHandlerRegistrar { return type.FullName ?? type.Name; } + + private enum GeneratedRegistrationResult + { + NoGeneratedRegistry, + FullyHandled, + RequiresReflectionFallback + } } diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 0392ac8a..0cd91844 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -61,6 +61,11 @@ public class CqrsHandlerRegistryGeneratorTests { public CqrsHandlerRegistryAttribute(Type registryType) { } } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + } } namespace TestApp @@ -120,10 +125,120 @@ public class CqrsHandlerRegistryGeneratorTests } /// - /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。 + /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器, + /// 并额外标记运行时补充反射扫描。 /// [Test] - public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_Handler() + public async Task + Generates_Visible_Handlers_And_Requests_Reflection_Fallback_When_Assembly_Contains_Private_Nested_Handler() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + const string expected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + [assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute()] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + typeof(global::TestApp.VisibleHandler)); + logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", expected)); + } + + /// + /// 验证当旧版 runtime 合同中不存在 reflection fallback 标记特性时, + /// 生成器会保留此前的整程序集回退行为,避免丢失不可见 handlers。 + /// + [Test] + public async Task Skips_Generation_For_Unsupported_Handler_When_Fallback_Marker_Is_Unavailable() { const string source = """ using System; diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 65aad69a..80561248 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -16,6 +16,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; + private const string CqrsReflectionFallbackAttributeMetadataName = + $"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute"; + private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; @@ -28,8 +31,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator /// public void Initialize(IncrementalGeneratorInitializationContext context) { - var generationEnabled = context.CompilationProvider - .Select(static (compilation, _) => HasRequiredTypes(compilation)); + var generationEnvironment = context.CompilationProvider + .Select(static (compilation, _) => CreateGenerationEnvironment(compilation)); // Restrict semantic analysis to type declarations that can actually contribute implemented interfaces. var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider( @@ -39,19 +42,24 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator .Collect(); context.RegisterSourceOutput( - generationEnabled.Combine(handlerCandidates), + generationEnvironment.Combine(handlerCandidates), static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right)); } - private static bool HasRequiredTypes(Compilation compilation) + private static GenerationEnvironment CreateGenerationEnvironment(Compilation compilation) { - return compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null && - compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null && - compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null && - compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null && - compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName) is not null && - compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && - compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; + var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null && + compilation.GetTypeByMetadataName( + CqrsHandlerRegistryAttributeMetadataName) is not null && + compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && + compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; + + return new GenerationEnvironment( + generationEnabled, + compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -108,21 +116,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator false); } - private static void Execute(SourceProductionContext context, bool generationEnabled, + private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, ImmutableArray candidates) { - if (!generationEnabled) + if (!generationEnvironment.GenerationEnabled) return; var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler); - // If the assembly contains handlers that generated code cannot legally reference - // (for example private nested handlers), keep the runtime on the reflection path - // so registration behavior remains complete instead of silently dropping handlers. - if (hasUnsupportedConcreteHandler || registrations.Count == 0) + if (registrations.Count == 0) return; - context.AddSource(HintName, GenerateSource(registrations)); + // If the runtime contract does not yet expose the reflection fallback marker, + // keep the previous all-or-nothing behavior so unsupported handlers are not silently dropped. + if (hasUnsupportedConcreteHandler && !generationEnvironment.SupportsReflectionFallbackMarker) + return; + + context.AddSource( + HintName, + GenerateSource(registrations, hasUnsupportedConcreteHandler)); } private static List CollectRegistrations( @@ -144,7 +156,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (candidate.Value.HasUnsupportedConcreteHandler) { hasUnsupportedConcreteHandler = true; - return []; + continue; } uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value; @@ -270,7 +282,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return GetTypeSortKey(type).Replace("global::", string.Empty); } - private static string GenerateSource(IReadOnlyList registrations) + private static string GenerateSource( + IReadOnlyList registrations, + bool emitReflectionFallbackAttribute) { var builder = new StringBuilder(); builder.AppendLine("// "); @@ -283,6 +297,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append('.'); builder.Append(GeneratedTypeName); builder.AppendLine("))]"); + if (emitReflectionFallbackAttribute) + { + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".CqrsReflectionFallbackAttribute()]"); + } + builder.AppendLine(); builder.Append("namespace "); builder.Append(GeneratedNamespace); @@ -399,4 +420,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } } } + + private readonly record struct GenerationEnvironment( + bool GenerationEnabled, + bool SupportsReflectionFallbackMarker); } From 00a1038d0a0801199cd0e1542c4f35adcdb820c5 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:52:39 +0800 Subject: [PATCH 53/70] =?UTF-8?q?refactor(GFramework.Cqrs):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=85=A8=E5=B1=80using=E5=BC=95=E7=94=A8System.Reflec?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在GlobalUsings.cs文件中新增System.Reflection的全局引用 - 便于后续代码中直接使用Reflection相关功能 - 减少重复的using声明语句 --- GFramework.Cqrs/GlobalUsings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs index 97f2d13a..b60938a5 100644 --- a/GFramework.Cqrs/GlobalUsings.cs +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -3,5 +3,6 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; +global using System.Reflection; global using Microsoft.Extensions.DependencyInjection; global using System.Diagnostics; From 0d9d09bc4ab4f06d54edad54a8a86b330c46a3f6 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:14:27 +0800 Subject: [PATCH 54/70] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0Microsoft?= =?UTF-8?q?=20DI=E5=AE=B9=E5=99=A8=E9=80=82=E9=85=8D=E5=99=A8=E5=8F=8A?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现MicrosoftDiContainer类作为IIocContainer接口的适配器 - 提供线程安全的依赖注入容器功能 - 支持单例、瞬态、作用域服务注册 - 实现CQRS处理器注册功能 - 添加服务工厂方法注册支持 - 实现按优先级排序的服务获取功能 - 添加完整的单元测试覆盖基本功能和边界情况 - 支持容器冻结和作用域创建功能 - 实现多样性实例注册到多个接口的功能 --- .../Ioc/MicrosoftDiContainerTests.cs | 62 +++++++ GFramework.Core/Ioc/MicrosoftDiContainer.cs | 168 ++++++++++++------ .../Cqrs/ICqrsRuntime.cs | 32 ++++ .../Internal/CqrsHandlerRegistrar.cs | 2 + .../DefaultCqrsRegistrationService.cs | 5 +- 5 files changed, 215 insertions(+), 54 deletions(-) diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 39445d48..4e3b5b23 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -249,6 +249,46 @@ public class MicrosoftDiContainerTests Assert.That(results.Count, Is.EqualTo(0)); } + /// + /// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复, + /// 但会保留同一服务类型的重复显式注册。 + /// + [Test] + public void GetAll_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases() + { + var instance = new AliasAwareService(); + + _container.Register(instance); + _container.Register(instance); + _container.Register(instance); + + var results = _container.GetAll(); + + Assert.That(results, Has.Count.EqualTo(2)); + Assert.That(results[0], Is.SameAs(instance)); + Assert.That(results[1], Is.SameAs(instance)); + } + + /// + /// 测试非泛型 GetAll 在容器未冻结时与泛型重载保持相同的别名去重语义。 + /// + [Test] + public void + GetAll_Type_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases() + { + var instance = new AliasAwareService(); + + _container.Register(instance); + _container.Register(instance); + _container.Register(instance); + + var results = _container.GetAll(typeof(ISharedAliasService)); + + Assert.That(results, Has.Count.EqualTo(2)); + Assert.That(results[0], Is.SameAs(instance)); + Assert.That(results[1], Is.SameAs(instance)); + } + /// /// 测试获取排序后的所有实例的功能 /// @@ -716,6 +756,28 @@ public interface IMixedService string? Name { get; set; } } +/// +/// 用于验证未冻结查询路径中的服务别名去重行为。 +/// +public interface ISharedAliasService; + +/// +/// 主服务别名接口。 +/// +public interface IPrimaryAliasService : ISharedAliasService; + +/// +/// 次级兼容别名接口。 +/// +public interface ISecondaryAliasService : ISharedAliasService; + +/// +/// 同时实现多个别名接口的测试服务。 +/// +public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService +{ +} + /// /// 实现优先级的服务 /// diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 78295ed4..5c49621a 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -35,6 +35,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) #endregion + /// + /// 记录某个实例在未冻结查询中可见的服务类型分组信息。 + /// + /// 当前分组对应的服务类型。 + /// 该服务类型下的描述符数量。 + /// 该服务类型首次出现的位置,用于稳定打破并列。 + private sealed record VisibleServiceTypeGroup(Type ServiceType, int Count, int FirstIndex); + #region Fields /// @@ -593,32 +601,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) { if (_provider == null) { - // 如果容器未冻结,从服务集合中获取已注册的实例 - var serviceType = typeof(T); - var registeredServices = GetServicesUnsafe - .Where(s => s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType)).ToList(); - - var result = new List(); - var seenInstances = new HashSet(ReferenceEqualityComparer.Instance); - foreach (var descriptor in registeredServices) - { - if (descriptor.ImplementationInstance is T instance) - { - // 同一实例可能同时以“正式接口 + 兼容别名接口”被注册;未冻结路径需去重以保持与冻结后的解析口径一致。 - if (seenInstances.Add(instance)) - result.Add(instance); - } - else if (descriptor.ImplementationFactory != null) - { - // 在未冻结状态下无法调用工厂方法,跳过 - } - else if (descriptor.ImplementationType != null) - { - // 在未冻结状态下无法创建实例,跳过 - } - } - - return result; + return CollectRegisteredImplementationInstances(typeof(T)).Cast().ToList(); } var services = _provider!.GetServices().ToList(); @@ -636,40 +619,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// /// 服务类型 /// 只读的服务实例列表 - /// 当容器未冻结时抛出 + /// 时抛出 public IReadOnlyList GetAll(Type type) { + ArgumentNullException.ThrowIfNull(type); + _lock.EnterReadLock(); try { if (_provider == null) { - // 如果容器未冻结,从服务集合中获取已注册的实例 - var registeredServices = GetServicesUnsafe - .Where(s => s.ServiceType == type || type.IsAssignableFrom(s.ServiceType)) - .ToList(); - - var result = new List(); - var seenInstances = new HashSet(ReferenceEqualityComparer.Instance); - foreach (var descriptor in registeredServices) - { - if (descriptor.ImplementationInstance != null) - { - // 同一实例可能通过多个可赋值服务类型暴露;返回前按引用去重,避免兼容别名造成重复观察结果。 - if (seenInstances.Add(descriptor.ImplementationInstance)) - result.Add(descriptor.ImplementationInstance); - } - else if (descriptor.ImplementationFactory != null) - { - // 在未冻结状态下无法调用工厂方法,跳过 - } - else if (descriptor.ImplementationType != null) - { - // 在未冻结状态下无法创建实例,跳过 - } - } - - return result; + return CollectRegisteredImplementationInstances(type); } var services = _provider!.GetServices(type).ToList(); @@ -682,6 +642,108 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 在容器未冻结时,从服务描述符中收集当前可直接观察到的实例绑定。 + /// + /// 调用方请求的服务类型。 + /// 按当前未冻结语义可见的实例列表。 + /// + /// 该方法只读取 ,因为未冻结路径不会主动执行工厂方法, + /// 也不会提前构造 。 + /// 若同一实例同时经由多个可赋值的 暴露, + /// 这里会把它视为兼容别名并只保留一个规范服务类型对应的结果; + /// 但同一 的重复显式注册仍会完整保留,以维持注册顺序和多次注册语义。 + /// + private List CollectRegisteredImplementationInstances(Type requestedServiceType) + { + ArgumentNullException.ThrowIfNull(requestedServiceType); + + var matchingDescriptors = GetServicesUnsafe + .Where(descriptor => + descriptor.ServiceType == requestedServiceType || + requestedServiceType.IsAssignableFrom(descriptor.ServiceType)) + .ToList(); + + if (matchingDescriptors.Count == 0) + return []; + + var preferredServiceTypes = BuildPreferredVisibleServiceTypes(matchingDescriptors, requestedServiceType); + var result = new List(); + foreach (var descriptor in matchingDescriptors) + { + if (descriptor.ImplementationInstance is { } instance) + { + if (preferredServiceTypes.TryGetValue(instance, out var preferredServiceType) && + preferredServiceType == descriptor.ServiceType) + { + result.Add(instance); + } + } + else if (descriptor.ImplementationFactory != null) + { + // 在未冻结状态下无法调用工厂方法,跳过。 + } + else if (descriptor.ImplementationType != null) + { + // 在未冻结状态下无法创建实例,跳过。 + } + } + + return result; + } + + /// + /// 为每个可见实例选择一个规范服务类型,避免同一实例因兼容别名重复出现在未冻结查询结果中。 + /// + /// 已按请求类型过滤过的服务描述符集合。 + /// 调用方请求的服务类型。 + /// 实例到其规范服务类型的映射。 + private static Dictionary BuildPreferredVisibleServiceTypes( + IReadOnlyList matchingDescriptors, + Type requestedServiceType) + { + var preferredServiceTypes = new Dictionary(ReferenceEqualityComparer.Instance); + foreach (var instanceGroup in matchingDescriptors + .Where(static descriptor => descriptor.ImplementationInstance is not null) + .GroupBy(static descriptor => descriptor.ImplementationInstance!, + ReferenceEqualityComparer.Instance)) + { + preferredServiceTypes.Add( + instanceGroup.Key, + SelectPreferredVisibleServiceType(instanceGroup, requestedServiceType)); + } + + return preferredServiceTypes; + } + + /// + /// 在“同一实例被多个服务类型暴露”的场景下,选择未冻结查询结果应保留的规范服务类型。 + /// + /// 引用同一实例的服务描述符。 + /// 调用方请求的服务类型。 + /// 应在结果中保留的服务类型。 + private static Type SelectPreferredVisibleServiceType( + IEnumerable descriptorsForInstance, + Type requestedServiceType) + { + var serviceTypeGroups = descriptorsForInstance + .GroupBy(static descriptor => descriptor.ServiceType) + .Select((group, index) => new VisibleServiceTypeGroup(group.Key, group.Count(), index)) + .ToList(); + + // 若调用方请求的正是其中一个服务类型,优先保留它,使未冻结行为尽量贴近冻结后的精确服务解析口径。 + var requestedGroup = serviceTypeGroups.FirstOrDefault(group => group.ServiceType == requestedServiceType); + if (requestedGroup is not null) + return requestedGroup.ServiceType; + + // 否则优先保留“同一服务类型下注册次数最多”的那组,避免显式多次注册被较宽泛的别名折叠掉。 + return serviceTypeGroups + .OrderByDescending(static group => group.Count) + .ThenBy(static group => group.FirstIndex) + .First() + .ServiceType; + } + /// /// 获取并排序指定泛型类型的所有服务实例 /// 主要用于系统调度场景 diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs index b8c85124..632af83e 100644 --- a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs @@ -15,6 +15,17 @@ public interface ICqrsRuntime /// 要分发的请求。 /// 取消令牌。 /// 请求响应。 + /// + /// 。 + /// + /// + /// 当前上下文无法满足运行时要求,例如未找到对应请求处理器,或请求处理链中的 + /// IContextAware 对象需要 IArchitectureContext 但当前 不提供该能力。 + /// + /// + /// 该契约允许调用方传入任意 , + /// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 IArchitectureContext。 + /// ValueTask SendAsync( ICqrsContext context, IRequest request, @@ -28,6 +39,16 @@ public interface ICqrsRuntime /// 要发布的通知。 /// 取消令牌。 /// 表示通知分发完成的值任务。 + /// + /// 。 + /// + /// + /// 已解析到的通知处理器需要框架级上下文注入,但当前 不提供 + /// IArchitectureContext 能力。 + /// + /// + /// 默认实现允许零处理器场景静默完成;只有在处理器注入前置条件不满足时才会抛出异常。 + /// ValueTask PublishAsync( ICqrsContext context, TNotification notification, @@ -42,6 +63,17 @@ public interface ICqrsRuntime /// 流式请求。 /// 取消令牌。 /// 按需生成的异步响应序列。 + /// + /// 。 + /// + /// + /// 当前上下文无法满足运行时要求,例如未找到对应流式处理器,或流式处理链中的 + /// IContextAware 对象需要 IArchitectureContext 但当前 不提供该能力。 + /// + /// + /// 返回的异步序列在枚举前通常已完成处理器解析与上下文注入, + /// 因此调用方应把 视为整个枚举生命周期内的必需依赖。 + /// IAsyncEnumerable CreateStream( ICqrsContext context, IStreamRequest request, diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 435c9cd5..867c6887 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -236,6 +236,8 @@ internal static class CqrsHandlerRegistrar Type handlerInterface, Type implementationType) { + // 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。 + // 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。 return services.Any(descriptor => descriptor.ServiceType == handlerInterface && descriptor.ImplementationType == implementationType); diff --git a/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs b/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs index 712ebea6..7993d748 100644 --- a/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs +++ b/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs @@ -1,4 +1,3 @@ -using System.Reflection; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; @@ -10,6 +9,10 @@ namespace GFramework.Cqrs.Internal; /// /// 该实现把“按稳定程序集键去重”和“委托给 handler registrar 执行实际映射注册”收敛到 CQRS runtime 内部, /// 避免外层容器继续了解 handler 注册流水线的内部结构。 +/// +/// 该类型不是线程安全的。调用方应在外部同步边界内访问 , +/// 例如由容器写锁串行化程序集注册流程。 +/// /// internal sealed class DefaultCqrsRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger) : ICqrsRegistrationService From a4dfc78201650a5fa847f51033913cbd76ddab18 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:26:05 +0800 Subject: [PATCH 55/70] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0Microsoft?= =?UTF-8?q?=20DI=E5=AE=B9=E5=99=A8=E9=80=82=E9=85=8D=E5=99=A8=E5=8F=8A?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现MicrosoftDiContainer类,包装IServiceProvider为IIocContainer接口 - 提供线程安全的依赖注入容器功能,支持单例、瞬态、作用域服务注册 - 添加RegisterSingleton、RegisterTransient、RegisterScoped等多种注册方法 - 实现RegisterPlurality方法支持一个实例注册到多个接口类型 - 添加CQRS相关注册功能,包括管道行为和处理器自动注册 - 实现Get、GetAll、GetRequired等服务解析方法 - 添加容器冻结机制,冻结后构建ServiceProvider提供服务解析 - 实现CreateScope方法支持服务作用域创建 - 添加完整的单元测试覆盖各种注册和解析场景 - 实现服务按优先级排序功能支持系统调度需求 --- .../Ioc/MicrosoftDiContainerTests.cs | 11 +++++++++++ GFramework.Core/Ioc/MicrosoftDiContainer.cs | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 4e3b5b23..19c59dcb 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -401,6 +401,17 @@ public class MicrosoftDiContainerTests Is.True); } + /// + /// 测试当程序集集合中包含空元素时,CQRS handler 注册入口会在委托给注册服务前直接失败。 + /// + [Test] + public void RegisterCqrsHandlersFromAssemblies_WithNullAssemblyItem_Should_ThrowArgumentNullException() + { + var assemblies = new Assembly[] { typeof(DeterministicOrderNotification).Assembly, null! }; + + Assert.Throws(() => _container.RegisterCqrsHandlersFromAssemblies(assemblies)); + } + /// /// 测试冻结容器以防止进一步注册的功能 /// diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 5c49621a..6152366f 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -399,16 +399,22 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// /// 要接入的程序集集合。 /// + /// 中存在 元素。 /// 容器已冻结,无法继续注册 CQRS 处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { ArgumentNullException.ThrowIfNull(assemblies); + var assemblyArray = assemblies.ToArray(); + foreach (var assembly in assemblyArray) + { + ArgumentNullException.ThrowIfNull(assembly); + } _lock.EnterWriteLock(); try { ThrowIfFrozen(); - ResolveCqrsRegistrationService().RegisterHandlers(assemblies); + ResolveCqrsRegistrationService().RegisterHandlers(assemblyArray); } finally { From 391e3e98138d5023192857c37eca201a5fee78b3 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:11:29 +0800 Subject: [PATCH 56/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E8=87=AA=E5=8A=A8=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现CqrsHandlerRegistrar类,支持扫描并注册CQRS请求/通知/流式处理器 - 添加源码生成注册器优先策略,减少冷启动时的反射开销 - 实现运行时反射扫描回退机制,确保处理器注册的完整性 - 添加CqrsReflectionFallbackAttribute特性,标记需要运行时补充扫描的程序集 - 创建完整的单元测试套件,验证处理器注册顺序与容错行为 - 实现CqrsHandlerRegistryGenerator源码生成器,自动生成处理器注册代码 - 添加详细的日志记录与诊断功能,便于调试注册过程 - 实现类型安全的处理器映射验证与重复注册检测机制 --- .../Cqrs/CqrsHandlerRegistrarTests.cs | 27 +++- .../CqrsReflectionFallbackAttribute.cs | 22 +++ .../Internal/CqrsHandlerRegistrar.cs | 152 +++++++++++++++--- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 111 +++++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 145 +++++++++++++++-- 5 files changed, 414 insertions(+), 43 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 318f6d21..95afa92f 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -190,11 +190,11 @@ internal sealed class CqrsHandlerRegistrarTests } /// - /// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers, - /// 同时避免把已由生成注册器注册的映射重复写入服务集合。 + /// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers, + /// 而不是重新枚举整个程序集的类型列表。 /// [Test] - public void RegisterHandlers_Should_Combine_Generated_Registry_With_Reflection_Fallback_Without_Duplicates() + public void RegisterHandlers_Should_Use_Targeted_Type_Lookups_For_Reflection_Fallback_Without_Duplicates() { var generatedAssembly = new Mock(); generatedAssembly @@ -205,14 +205,17 @@ internal sealed class CqrsHandlerRegistrarTests .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); generatedAssembly .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) - .Returns([new CqrsReflectionFallbackAttribute()]); - generatedAssembly - .Setup(static assembly => assembly.GetTypes()) .Returns( [ - typeof(GeneratedRegistryNotificationHandler), - ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + new CqrsReflectionFallbackAttribute( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!) ]); + generatedAssembly + .Setup(static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false)) + .Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType); var container = new MicrosoftDiContainer(); CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); @@ -231,6 +234,14 @@ internal sealed class CqrsHandlerRegistrarTests typeof(GeneratedRegistryNotificationHandler), ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType ])); + + generatedAssembly.Verify( + static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false), + Times.Once); + generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); } } diff --git a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs index f18a7344..9d3c21bf 100644 --- a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs +++ b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs @@ -11,4 +11,26 @@ namespace GFramework.Cqrs; [AttributeUsage(AttributeTargets.Assembly)] public sealed class CqrsReflectionFallbackAttribute : Attribute { + /// + /// 初始化 。 + /// + /// + /// 需要运行时补充反射注册的处理器类型全名。 + /// 当该清单为空时,运行时会回退到整程序集扫描,以兼容旧版 marker 语义。 + /// + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) + { + ArgumentNullException.ThrowIfNull(fallbackHandlerTypeNames); + + FallbackHandlerTypeNames = fallbackHandlerTypeNames + .Where(static typeName => !string.IsNullOrWhiteSpace(typeName)) + .Distinct(StringComparer.Ordinal) + .OrderBy(static typeName => typeName, StringComparer.Ordinal) + .ToArray(); + } + + /// + /// 获取需要运行时补充反射注册的处理器类型全名集合。 + /// + public IReadOnlyList FallbackHandlerTypeNames { get; } } diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 867c6887..968424b7 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -33,10 +33,14 @@ internal static class CqrsHandlerRegistrar { var generatedRegistrationResult = TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger); - if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled) + if (generatedRegistrationResult is { UsedGeneratedRegistry: true, RequiresReflectionFallback: false }) continue; - RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger); + RegisterAssemblyHandlers( + container.GetServicesUnsafe, + assembly, + logger, + generatedRegistrationResult.ReflectionFallbackTypeNames); } } @@ -66,7 +70,7 @@ internal static class CqrsHandlerRegistrar .ToList(); if (registryTypes.Count == 0) - return GeneratedRegistrationResult.NoGeneratedRegistry; + return GeneratedRegistrationResult.NoGeneratedRegistry(); var registries = new List(registryTypes.Count); foreach (var registryType in registryTypes) @@ -75,21 +79,21 @@ internal static class CqrsHandlerRegistrar { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); - return GeneratedRegistrationResult.NoGeneratedRegistry; + return GeneratedRegistrationResult.NoGeneratedRegistry(); } if (registryType.IsAbstract) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); - return GeneratedRegistrationResult.NoGeneratedRegistry; + return GeneratedRegistrationResult.NoGeneratedRegistry(); } if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated."); - return GeneratedRegistrationResult.NoGeneratedRegistry; + return GeneratedRegistrationResult.NoGeneratedRegistry(); } registries.Add(registry); @@ -102,14 +106,24 @@ internal static class CqrsHandlerRegistrar registry.Register(services, logger); } - if (RequiresReflectionFallback(assembly)) + var reflectionFallbackTypeNames = GetReflectionFallbackTypeNames(assembly); + if (reflectionFallbackTypeNames is not null) { - logger.Debug( - $"Generated CQRS registry for assembly {assemblyName} requested reflection fallback for unsupported handlers."); - return GeneratedRegistrationResult.RequiresReflectionFallback; + if (reflectionFallbackTypeNames.Count > 0) + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackTypeNames.Count} unsupported handler type(s)."); + } + else + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers."); + } + + return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackTypeNames); } - return GeneratedRegistrationResult.FullyHandled; + return GeneratedRegistrationResult.FullyHandled(); } catch (Exception exception) { @@ -117,16 +131,21 @@ internal static class CqrsHandlerRegistrar $"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan."); logger.Warn( $"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}"); - return GeneratedRegistrationResult.NoGeneratedRegistry; + return GeneratedRegistrationResult.NoGeneratedRegistry(); } } /// /// 注册单个程序集里的所有 CQRS 处理器映射。 /// - private static void RegisterAssemblyHandlers(IServiceCollection services, Assembly assembly, ILogger logger) + private static void RegisterAssemblyHandlers( + IServiceCollection services, + Assembly assembly, + ILogger logger, + IReadOnlyList? reflectionFallbackTypeNames) { - foreach (var implementationType in GetLoadableTypes(assembly, logger).Where(IsConcreteHandlerType)) + foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackTypeNames) + .Where(IsConcreteHandlerType)) { var handlerInterfaces = implementationType .GetInterfaces() @@ -155,6 +174,58 @@ internal static class CqrsHandlerRegistrar } } + /// + /// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。 + /// + private static IReadOnlyList GetCandidateHandlerTypes( + Assembly assembly, + ILogger logger, + IReadOnlyList? reflectionFallbackTypeNames) + { + return reflectionFallbackTypeNames is { Count: > 0 } + ? GetNamedFallbackTypes(assembly, reflectionFallbackTypeNames, logger) + : GetLoadableTypes(assembly, logger); + } + + /// + /// 根据生成器记录的类型全名,精确解析仍需运行时补充注册的处理器类型。 + /// + private static IReadOnlyList GetNamedFallbackTypes( + Assembly assembly, + IReadOnlyList reflectionFallbackTypeNames, + ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + var resolvedTypes = new List(reflectionFallbackTypeNames.Count); + foreach (var typeName in reflectionFallbackTypeNames + .Where(static name => !string.IsNullOrWhiteSpace(name)) + .Distinct(StringComparer.Ordinal) + .OrderBy(static name => name, StringComparer.Ordinal)) + { + try + { + var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false); + if (type is null) + { + logger.Warn( + $"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry."); + continue; + } + + resolvedTypes.Add(type); + } + catch (Exception exception) + { + logger.Warn( + $"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}"); + } + } + + return resolvedTypes + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + } + /// /// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。 /// @@ -221,11 +292,24 @@ internal static class CqrsHandlerRegistrar } /// - /// 判断生成注册器是否要求运行时继续补充反射扫描。 + /// 获取生成注册器要求运行时继续补充反射扫描的 handler 类型名清单。 /// - private static bool RequiresReflectionFallback(Assembly assembly) + private static IReadOnlyList? GetReflectionFallbackTypeNames(Assembly assembly) { - return assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)?.Length > 0; + var fallbackAttributes = assembly + .GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false) + .OfType() + .ToList(); + + if (fallbackAttributes.Count == 0) + return null; + + return fallbackAttributes + .SelectMany(static attribute => attribute.FallbackHandlerTypeNames) + .Where(static typeName => !string.IsNullOrWhiteSpace(typeName)) + .Distinct(StringComparer.Ordinal) + .OrderBy(static typeName => typeName, StringComparer.Ordinal) + .ToArray(); } /// @@ -259,10 +343,36 @@ internal static class CqrsHandlerRegistrar return type.FullName ?? type.Name; } - private enum GeneratedRegistrationResult + private readonly record struct GeneratedRegistrationResult( + bool UsedGeneratedRegistry, + bool RequiresReflectionFallback, + IReadOnlyList? ReflectionFallbackTypeNames) { - NoGeneratedRegistry, - FullyHandled, - RequiresReflectionFallback + public static GeneratedRegistrationResult NoGeneratedRegistry() + { + return new GeneratedRegistrationResult( + UsedGeneratedRegistry: false, + RequiresReflectionFallback: false, + ReflectionFallbackTypeNames: null); + } + + public static GeneratedRegistrationResult FullyHandled() + { + return new GeneratedRegistrationResult( + UsedGeneratedRegistry: true, + RequiresReflectionFallback: false, + ReflectionFallbackTypeNames: null); + } + + public static GeneratedRegistrationResult WithReflectionFallback( + IReadOnlyList reflectionFallbackTypeNames) + { + ArgumentNullException.ThrowIfNull(reflectionFallbackTypeNames); + + return new GeneratedRegistrationResult( + UsedGeneratedRegistry: true, + RequiresReflectionFallback: true, + ReflectionFallbackTypeNames: reflectionFallbackTypeNames); + } } } diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 0cd91844..e1ec1546 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -65,6 +65,7 @@ public class CqrsHandlerRegistryGeneratorTests [AttributeUsage(AttributeTargets.Assembly)] public sealed class CqrsReflectionFallbackAttribute : Attribute { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } } } @@ -180,6 +181,116 @@ public class CqrsHandlerRegistryGeneratorTests [AttributeUsage(AttributeTargets.Assembly)] public sealed class CqrsReflectionFallbackAttribute : Attribute { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + const string expected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + [assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute("TestApp.Container+HiddenHandler")] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + typeof(global::TestApp.VisibleHandler)); + logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", expected)); + } + + /// + /// 验证当 runtime 仅支持旧版无参 fallback marker 时,生成器会退回旧语义, + /// 只输出 marker 而不输出精确类型名。 + /// + [Test] + public async Task Generates_Legacy_Fallback_Marker_When_Runtime_Does_Not_Support_Type_Name_List() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute() { } } } diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 80561248..1e260e32 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -59,7 +59,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return new GenerationEnvironment( generationEnabled, - compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null); + GetReflectionFallbackEmissionMode( + compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName))); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -96,7 +97,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return new HandlerCandidateAnalysis( implementationTypeDisplayName, ImmutableArray.Empty, - true); + true, + GetReflectionFallbackTypeName(type)); } var implementationLogName = GetLogDisplayName(type); @@ -113,7 +115,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return new HandlerCandidateAnalysis( implementationTypeDisplayName, registrations.MoveToImmutable(), - false); + false, + null); } private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, @@ -122,27 +125,37 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (!generationEnvironment.GenerationEnabled) return; - var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler); + var registrations = CollectRegistrations( + candidates, + out var hasUnsupportedConcreteHandler, + out var reflectionFallbackTypeNames); if (registrations.Count == 0) return; // If the runtime contract does not yet expose the reflection fallback marker, // keep the previous all-or-nothing behavior so unsupported handlers are not silently dropped. - if (hasUnsupportedConcreteHandler && !generationEnvironment.SupportsReflectionFallbackMarker) + if (hasUnsupportedConcreteHandler && + generationEnvironment.ReflectionFallbackEmissionMode == ReflectionFallbackEmissionMode.Disabled) return; context.AddSource( HintName, - GenerateSource(registrations, hasUnsupportedConcreteHandler)); + GenerateSource( + registrations, + hasUnsupportedConcreteHandler, + generationEnvironment.ReflectionFallbackEmissionMode, + reflectionFallbackTypeNames)); } private static List CollectRegistrations( ImmutableArray candidates, - out bool hasUnsupportedConcreteHandler) + out bool hasUnsupportedConcreteHandler, + out IReadOnlyList reflectionFallbackTypeNames) { var registrations = new List(); hasUnsupportedConcreteHandler = false; + var fallbackTypeNames = new SortedSet(StringComparer.Ordinal); // Partial declarations surface the same symbol through multiple syntax nodes. // Collapse them by implementation type so generated registrations stay stable and duplicate-free. @@ -156,6 +169,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (candidate.Value.HasUnsupportedConcreteHandler) { hasUnsupportedConcreteHandler = true; + var reflectionFallbackTypeName = candidate.Value.ReflectionFallbackTypeName; + if (reflectionFallbackTypeName is not null && + !string.IsNullOrWhiteSpace(reflectionFallbackTypeName)) + { + fallbackTypeNames.Add(reflectionFallbackTypeName); + } + continue; } @@ -178,9 +198,30 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator : StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName); }); + reflectionFallbackTypeNames = fallbackTypeNames.ToArray(); return registrations; } + private static ReflectionFallbackEmissionMode GetReflectionFallbackEmissionMode(INamedTypeSymbol? attributeType) + { + if (attributeType is null) + return ReflectionFallbackEmissionMode.Disabled; + + foreach (var constructor in attributeType.InstanceConstructors) + { + if (constructor.Parameters.Length != 1) + continue; + + if (constructor.Parameters[0].Type is IArrayTypeSymbol arrayType && + arrayType.ElementType.SpecialType == SpecialType.System_String) + { + return ReflectionFallbackEmissionMode.PreciseTypeNames; + } + } + + return ReflectionFallbackEmissionMode.MarkerOnly; + } + private static bool IsConcreteHandlerType(INamedTypeSymbol type) { return type.TypeKind is TypeKind.Class or TypeKind.Struct && @@ -272,6 +313,34 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return builder.ToString(); } + private static string GetReflectionFallbackTypeName(INamedTypeSymbol type) + { + var nestedTypes = new Stack(); + for (var current = type; current is not null; current = current.ContainingType) + { + nestedTypes.Push(current.MetadataName); + } + + var builder = new StringBuilder(); + if (!type.ContainingNamespace.IsGlobalNamespace) + { + builder.Append(type.ContainingNamespace.ToDisplayString()); + builder.Append('.'); + } + + var isFirstType = true; + while (nestedTypes.Count > 0) + { + if (!isFirstType) + builder.Append('+'); + + builder.Append(nestedTypes.Pop()); + isFirstType = false; + } + + return builder.ToString(); + } + private static string GetTypeSortKey(ITypeSymbol type) { return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); @@ -284,7 +353,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private static string GenerateSource( IReadOnlyList registrations, - bool emitReflectionFallbackAttribute) + bool emitReflectionFallbackAttribute, + ReflectionFallbackEmissionMode reflectionFallbackEmissionMode, + IReadOnlyList reflectionFallbackTypeNames) { var builder = new StringBuilder(); builder.AppendLine("// "); @@ -297,11 +368,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append('.'); builder.Append(GeneratedTypeName); builder.AppendLine("))]"); - if (emitReflectionFallbackAttribute) + if (emitReflectionFallbackAttribute && + reflectionFallbackEmissionMode != ReflectionFallbackEmissionMode.Disabled) { - builder.Append("[assembly: global::"); - builder.Append(CqrsRuntimeNamespace); - builder.AppendLine(".CqrsReflectionFallbackAttribute()]"); + AppendReflectionFallbackAttribute(builder, reflectionFallbackEmissionMode, reflectionFallbackTypeNames); } builder.AppendLine(); @@ -349,6 +419,36 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return builder.ToString(); } + private static void AppendReflectionFallbackAttribute( + StringBuilder builder, + ReflectionFallbackEmissionMode reflectionFallbackEmissionMode, + IReadOnlyList reflectionFallbackTypeNames) + { + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsReflectionFallbackAttribute"); + + if (reflectionFallbackEmissionMode == ReflectionFallbackEmissionMode.PreciseTypeNames && + reflectionFallbackTypeNames.Count > 0) + { + builder.Append('('); + for (var index = 0; index < reflectionFallbackTypeNames.Count; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append('"'); + builder.Append(EscapeStringLiteral(reflectionFallbackTypeNames[index])); + builder.Append('"'); + } + + builder.AppendLine(")]"); + return; + } + + builder.AppendLine("()]"); + } + private static string EscapeStringLiteral(string value) { return value.Replace("\\", "\\\\") @@ -368,11 +468,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public HandlerCandidateAnalysis( string implementationTypeDisplayName, ImmutableArray registrations, - bool hasUnsupportedConcreteHandler) + bool hasUnsupportedConcreteHandler, + string? reflectionFallbackTypeName) { ImplementationTypeDisplayName = implementationTypeDisplayName; Registrations = registrations; HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler; + ReflectionFallbackTypeName = reflectionFallbackTypeName; } public string ImplementationTypeDisplayName { get; } @@ -381,11 +483,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public bool HasUnsupportedConcreteHandler { get; } + public string? ReflectionFallbackTypeName { get; } + public bool Equals(HandlerCandidateAnalysis other) { if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, StringComparison.Ordinal) || HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler || + !string.Equals(ReflectionFallbackTypeName, other.ReflectionFallbackTypeName, + StringComparison.Ordinal) || Registrations.Length != other.Registrations.Length) { return false; @@ -411,6 +517,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName); hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode(); + hashCode = (hashCode * 397) ^ + (ReflectionFallbackTypeName is null + ? 0 + : StringComparer.Ordinal.GetHashCode(ReflectionFallbackTypeName)); foreach (var registration in Registrations) { hashCode = (hashCode * 397) ^ registration.GetHashCode(); @@ -423,5 +533,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private readonly record struct GenerationEnvironment( bool GenerationEnabled, - bool SupportsReflectionFallbackMarker); + ReflectionFallbackEmissionMode ReflectionFallbackEmissionMode); + + private enum ReflectionFallbackEmissionMode + { + Disabled, + MarkerOnly, + PreciseTypeNames + } } From 06f95db5933a82d173c6f9930db5dd20e3746baa Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:36:31 +0800 Subject: [PATCH 57/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E5=99=A8=E5=AE=9E=E7=8E=B0=E5=92=8C=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现GFramework自有CQRS运行时分发器,支持请求/通知/流式请求处理 - 添加进程级缓存机制优化反射调用性能,包括请求、通知、流水线调用委托缓存 - 重构CqrsHandlerRegistrar使用ReflectionFallbackMetadata替代字符串类型名 - 引入CqrsReflectionFallbackAttribute支持运行时补充反射扫描的处理器类型 - 添加完整的CQRS处理器注册单元测试,验证有序执行和容错行为 - 修复MicrosoftDiContainer中异常消息的格式化空白问题 - 实现上下文感知处理器的CQRS分发上下文注入功能 --- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 2 +- .../Cqrs/CqrsDispatcherCacheTests.cs | 172 ++++++++++++++++++ .../Cqrs/CqrsHandlerRegistrarTests.cs | 49 +++++ .../CqrsReflectionFallbackAttribute.cs | 35 ++++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 34 +++- .../Internal/CqrsHandlerRegistrar.cs | 107 ++++++----- 6 files changed, 345 insertions(+), 54 deletions(-) create mode 100644 GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 6152366f..d1a0576d 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -400,7 +400,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 要接入的程序集集合。 /// /// 中存在 元素。 - /// 容器已冻结,无法继续注册 CQRS 处理器。 + /// 容器已冻结,无法继续注册 CQRS 处理器。 public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { ArgumentNullException.ThrowIfNull(assemblies); diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs new file mode 100644 index 00000000..6c52a910 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -0,0 +1,172 @@ +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; + +/// +/// 验证 CQRS dispatcher 会缓存热路径中的服务类型构造结果。 +/// +[TestFixture] +internal sealed class CqrsDispatcherCacheTests +{ + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + + /// + /// 初始化测试上下文。 + /// + [SetUp] + public void SetUp() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + _container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers( + _container, + typeof(CqrsDispatcherCacheTests).Assembly, + typeof(ArchitectureContext).Assembly); + + _container.Freeze(); + _context = new ArchitectureContext(_container); + } + + /// + /// 清理测试上下文引用。 + /// + [TearDown] + public void TearDown() + { + _context = null; + _container = null; + } + + /// + /// 验证相同消息类型重复分发时,不会重复扩张服务类型缓存。 + /// + [Test] + public async Task Dispatcher_Should_Cache_Service_Types_After_First_Dispatch() + { + var notificationServiceTypes = GetCacheField("NotificationHandlerServiceTypes"); + var requestServiceTypes = GetCacheField("RequestServiceTypes"); + var streamServiceTypes = GetCacheField("StreamHandlerServiceTypes"); + + var notificationBefore = notificationServiceTypes.Count; + var requestBefore = requestServiceTypes.Count; + var streamBefore = streamServiceTypes.Count; + + await _context!.SendRequestAsync(new DispatcherCacheRequest()); + await _context.PublishAsync(new DispatcherCacheNotification()); + await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest())); + + var notificationAfterFirstDispatch = notificationServiceTypes.Count; + var requestAfterFirstDispatch = requestServiceTypes.Count; + var streamAfterFirstDispatch = streamServiceTypes.Count; + + await _context.SendRequestAsync(new DispatcherCacheRequest()); + await _context.PublishAsync(new DispatcherCacheNotification()); + await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest())); + + Assert.Multiple(() => + { + Assert.That(notificationAfterFirstDispatch, Is.EqualTo(notificationBefore + 1)); + Assert.That(requestAfterFirstDispatch, Is.EqualTo(requestBefore + 1)); + Assert.That(streamAfterFirstDispatch, Is.EqualTo(streamBefore + 1)); + + Assert.That(notificationServiceTypes.Count, Is.EqualTo(notificationAfterFirstDispatch)); + Assert.That(requestServiceTypes.Count, Is.EqualTo(requestAfterFirstDispatch)); + Assert.That(streamServiceTypes.Count, Is.EqualTo(streamAfterFirstDispatch)); + }); + } + + /// + /// 通过反射读取 dispatcher 的静态缓存字典。 + /// + private static IDictionary GetCacheField(string fieldName) + { + var dispatcherType = typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!; + + var field = dispatcherType.GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}."); + + return field!.GetValue(null) as IDictionary + ?? throw new InvalidOperationException( + $"Dispatcher cache field {fieldName} does not implement IDictionary."); + } + + /// + /// 消费整个异步流,确保建流路径被真实执行。 + /// + private static async Task DrainAsync(IAsyncEnumerable stream) + { + await foreach (var _ in stream) + { + } + } +} + +/// +/// 用于验证 request 服务类型缓存的测试请求。 +/// +internal sealed record DispatcherCacheRequest : IRequest; + +/// +/// 用于验证 notification 服务类型缓存的测试通知。 +/// +internal sealed record DispatcherCacheNotification : INotification; + +/// +/// 用于验证 stream 服务类型缓存的测试请求。 +/// +internal sealed record DispatcherCacheStreamRequest : IStreamRequest; + +/// +/// 处理 。 +/// +internal sealed class DispatcherCacheRequestHandler : IRequestHandler +{ + /// + /// 返回固定结果,供缓存测试验证 dispatcher 请求路径。 + /// + public ValueTask Handle(DispatcherCacheRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(1); + } +} + +/// +/// 处理 。 +/// +internal sealed class DispatcherCacheNotificationHandler : INotificationHandler +{ + /// + /// 消费通知,不执行额外副作用。 + /// + public ValueTask Handle(DispatcherCacheNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} + +/// +/// 处理 。 +/// +internal sealed class DispatcherCacheStreamHandler : IStreamRequestHandler +{ + /// + /// 返回一个最小流,供缓存测试命中 stream 分发路径。 + /// + public async IAsyncEnumerable Handle( + DispatcherCacheStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return 1; + await Task.CompletedTask; + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 95afa92f..b44b0bb1 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -243,6 +243,55 @@ internal sealed class CqrsHandlerRegistrarTests Times.Once); generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); } + + /// + /// 验证手写 fallback metadata 直接提供 handler 类型时,运行时会复用这些类型, + /// 而不会再通过程序集名称查找或整程序集扫描补齐映射。 + /// + [Test] + public void RegisterHandlers_Should_Use_Direct_Fallback_Types_Without_GetType_Or_GetTypes() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.Assembly.FullName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false)) + .Returns( + [ + new CqrsReflectionFallbackAttribute( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType) + ]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + var registrations = container.GetServicesUnsafe + .Where(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler) && + descriptor.ImplementationType is not null) + .Select(static descriptor => descriptor.ImplementationType!) + .ToList(); + + Assert.That( + registrations, + Is.EqualTo( + [ + typeof(GeneratedRegistryNotificationHandler), + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + ])); + + generatedAssembly.Verify( + static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false), + Times.Never); + generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); + } } /// diff --git a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs index 9d3c21bf..da557d84 100644 --- a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs +++ b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs @@ -11,6 +11,15 @@ namespace GFramework.Cqrs; [AttributeUsage(AttributeTargets.Assembly)] public sealed class CqrsReflectionFallbackAttribute : Attribute { + /// + /// 初始化 ,保留旧版“仅标记需要补扫”的语义。 + /// + public CqrsReflectionFallbackAttribute() + { + FallbackHandlerTypeNames = []; + FallbackHandlerTypes = []; + } + /// /// 初始化 。 /// @@ -27,10 +36,36 @@ public sealed class CqrsReflectionFallbackAttribute : Attribute .Distinct(StringComparer.Ordinal) .OrderBy(static typeName => typeName, StringComparer.Ordinal) .ToArray(); + FallbackHandlerTypes = []; + } + + /// + /// 初始化 。 + /// + /// + /// 需要运行时补充反射注册的处理器类型。 + /// 该重载适合手写或第三方程序集显式声明可直接引用的 fallback handlers, + /// 避免再通过字符串名称回查程序集元数据。 + /// + public CqrsReflectionFallbackAttribute(params Type[] fallbackHandlerTypes) + { + ArgumentNullException.ThrowIfNull(fallbackHandlerTypes); + + FallbackHandlerTypeNames = []; + FallbackHandlerTypes = fallbackHandlerTypes + .Where(static type => type is not null) + .Distinct() + .OrderBy(static type => type.FullName ?? type.Name, StringComparer.Ordinal) + .ToArray(); } /// /// 获取需要运行时补充反射注册的处理器类型全名集合。 /// public IReadOnlyList FallbackHandlerTypeNames { get; } + + /// + /// 获取可直接供运行时补充反射注册的处理器类型集合。 + /// + public IReadOnlyList FallbackHandlerTypes { get; } } diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 9a125789..91532e17 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -1,5 +1,3 @@ -using System.Collections.Concurrent; -using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; @@ -30,10 +28,22 @@ internal sealed class CqrsDispatcher( // 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。 private static readonly ConcurrentDictionary NotificationInvokers = new(); + // 进程级缓存:缓存通知处理器服务类型,避免每次发布都重复 MakeGenericType。 + private static readonly ConcurrentDictionary NotificationHandlerServiceTypes = new(); + // 进程级缓存:缓存流式请求调用委托,避免每次创建流时重复解析反射签名。 private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new(); + // 进程级缓存:缓存请求处理器与 pipeline 行为的服务类型,减少热路径中的泛型类型构造。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestServiceTypeSet> + RequestServiceTypes = new(); + + // 进程级缓存:缓存流式请求处理器服务类型,避免每次建流时重复 MakeGenericType。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), Type> + StreamHandlerServiceTypes = + new(); + /// /// 发布通知到所有已注册处理器。 /// @@ -51,7 +61,9 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(notification); var notificationType = notification.GetType(); - var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); + var handlerType = NotificationHandlerServiceTypes.GetOrAdd( + notificationType, + static type => typeof(INotificationHandler<>).MakeGenericType(type)); var handlers = container.GetAll(handlerType); if (handlers.Count == 0) @@ -88,14 +100,18 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); - var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)); + var serviceTypes = RequestServiceTypes.GetOrAdd( + (requestType, typeof(TResponse)), + static key => new RequestServiceTypeSet( + typeof(IRequestHandler<,>).MakeGenericType(key.RequestType, key.ResponseType), + typeof(IPipelineBehavior<,>).MakeGenericType(key.RequestType, key.ResponseType))); + var handlerType = serviceTypes.HandlerType; var handler = container.Get(handlerType) ?? throw new InvalidOperationException( $"No CQRS request handler registered for {requestType.FullName}."); PrepareHandler(handler, context); - var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)); - var behaviors = container.GetAll(behaviorType); + var behaviors = container.GetAll(serviceTypes.BehaviorType); foreach (var behavior in behaviors) PrepareHandler(behavior, context); @@ -135,7 +151,9 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); - var handlerType = typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)); + var handlerType = StreamHandlerServiceTypes.GetOrAdd( + (requestType, typeof(TResponse)), + static key => typeof(IStreamRequestHandler<,>).MakeGenericType(key.RequestType, key.ResponseType)); var handler = container.Get(handlerType) ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); @@ -293,4 +311,6 @@ internal sealed class CqrsDispatcher( CancellationToken cancellationToken); private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + + private readonly record struct RequestServiceTypeSet(Type HandlerType, Type BehaviorType); } diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 968424b7..3604de83 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -40,7 +40,7 @@ internal static class CqrsHandlerRegistrar container.GetServicesUnsafe, assembly, logger, - generatedRegistrationResult.ReflectionFallbackTypeNames); + generatedRegistrationResult.ReflectionFallbackMetadata); } } @@ -106,13 +106,13 @@ internal static class CqrsHandlerRegistrar registry.Register(services, logger); } - var reflectionFallbackTypeNames = GetReflectionFallbackTypeNames(assembly); - if (reflectionFallbackTypeNames is not null) + var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger); + if (reflectionFallbackMetadata is not null) { - if (reflectionFallbackTypeNames.Count > 0) + if (reflectionFallbackMetadata.HasExplicitTypes) { logger.Debug( - $"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackTypeNames.Count} unsupported handler type(s)."); + $"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s)."); } else { @@ -120,7 +120,7 @@ internal static class CqrsHandlerRegistrar $"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers."); } - return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackTypeNames); + return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata); } return GeneratedRegistrationResult.FullyHandled(); @@ -142,9 +142,9 @@ internal static class CqrsHandlerRegistrar IServiceCollection services, Assembly assembly, ILogger logger, - IReadOnlyList? reflectionFallbackTypeNames) + ReflectionFallbackMetadata? reflectionFallbackMetadata) { - foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackTypeNames) + foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata) .Where(IsConcreteHandlerType)) { var handlerInterfaces = implementationType @@ -180,24 +180,51 @@ internal static class CqrsHandlerRegistrar private static IReadOnlyList GetCandidateHandlerTypes( Assembly assembly, ILogger logger, - IReadOnlyList? reflectionFallbackTypeNames) + ReflectionFallbackMetadata? reflectionFallbackMetadata) { - return reflectionFallbackTypeNames is { Count: > 0 } - ? GetNamedFallbackTypes(assembly, reflectionFallbackTypeNames, logger) + return reflectionFallbackMetadata is { HasExplicitTypes: true } + ? reflectionFallbackMetadata.Types : GetLoadableTypes(assembly, logger); } /// - /// 根据生成器记录的类型全名,精确解析仍需运行时补充注册的处理器类型。 + /// 获取生成注册器要求运行时继续补充反射扫描的 handler 元数据。 /// - private static IReadOnlyList GetNamedFallbackTypes( + private static ReflectionFallbackMetadata? GetReflectionFallbackMetadata( Assembly assembly, - IReadOnlyList reflectionFallbackTypeNames, ILogger logger) { var assemblyName = GetAssemblySortKey(assembly); - var resolvedTypes = new List(reflectionFallbackTypeNames.Count); - foreach (var typeName in reflectionFallbackTypeNames + var fallbackAttributes = assembly + .GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false) + .OfType() + .ToList(); + + if (fallbackAttributes.Count == 0) + return null; + + var resolvedTypes = new List(); + foreach (var fallbackType in fallbackAttributes + .SelectMany(static attribute => attribute.FallbackHandlerTypes) + .Where(static type => type is not null) + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal)) + { + if (!string.Equals( + GetAssemblySortKey(fallbackType.Assembly), + assemblyName, + StringComparison.Ordinal)) + { + logger.Warn( + $"Generated CQRS reflection fallback type {fallbackType.FullName} was declared on assembly {assemblyName} but belongs to assembly {GetAssemblySortKey(fallbackType.Assembly)}. Skipping mismatched fallback entry."); + continue; + } + + resolvedTypes.Add(fallbackType); + } + + foreach (var typeName in fallbackAttributes + .SelectMany(static attribute => attribute.FallbackHandlerTypeNames) .Where(static name => !string.IsNullOrWhiteSpace(name)) .Distinct(StringComparer.Ordinal) .OrderBy(static name => name, StringComparer.Ordinal)) @@ -221,9 +248,11 @@ internal static class CqrsHandlerRegistrar } } - return resolvedTypes - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); + return new ReflectionFallbackMetadata( + resolvedTypes + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray()); } /// @@ -291,27 +320,6 @@ internal static class CqrsHandlerRegistrar definition == typeof(IStreamRequestHandler<,>); } - /// - /// 获取生成注册器要求运行时继续补充反射扫描的 handler 类型名清单。 - /// - private static IReadOnlyList? GetReflectionFallbackTypeNames(Assembly assembly) - { - var fallbackAttributes = assembly - .GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false) - .OfType() - .ToList(); - - if (fallbackAttributes.Count == 0) - return null; - - return fallbackAttributes - .SelectMany(static attribute => attribute.FallbackHandlerTypeNames) - .Where(static typeName => !string.IsNullOrWhiteSpace(typeName)) - .Distinct(StringComparer.Ordinal) - .OrderBy(static typeName => typeName, StringComparer.Ordinal) - .ToArray(); - } - /// /// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。 /// @@ -346,14 +354,14 @@ internal static class CqrsHandlerRegistrar private readonly record struct GeneratedRegistrationResult( bool UsedGeneratedRegistry, bool RequiresReflectionFallback, - IReadOnlyList? ReflectionFallbackTypeNames) + ReflectionFallbackMetadata? ReflectionFallbackMetadata) { public static GeneratedRegistrationResult NoGeneratedRegistry() { return new GeneratedRegistrationResult( UsedGeneratedRegistry: false, RequiresReflectionFallback: false, - ReflectionFallbackTypeNames: null); + ReflectionFallbackMetadata: null); } public static GeneratedRegistrationResult FullyHandled() @@ -361,18 +369,25 @@ internal static class CqrsHandlerRegistrar return new GeneratedRegistrationResult( UsedGeneratedRegistry: true, RequiresReflectionFallback: false, - ReflectionFallbackTypeNames: null); + ReflectionFallbackMetadata: null); } public static GeneratedRegistrationResult WithReflectionFallback( - IReadOnlyList reflectionFallbackTypeNames) + ReflectionFallbackMetadata reflectionFallbackMetadata) { - ArgumentNullException.ThrowIfNull(reflectionFallbackTypeNames); + ArgumentNullException.ThrowIfNull(reflectionFallbackMetadata); return new GeneratedRegistrationResult( UsedGeneratedRegistry: true, RequiresReflectionFallback: true, - ReflectionFallbackTypeNames: reflectionFallbackTypeNames); + ReflectionFallbackMetadata: reflectionFallbackMetadata); } } + + private sealed class ReflectionFallbackMetadata(IReadOnlyList types) + { + public IReadOnlyList Types { get; } = types ?? throw new ArgumentNullException(nameof(types)); + + public bool HasExplicitTypes => Types.Count > 0; + } } From b07da252c48ebc66875315eb6c4407ffca0ce77e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:53:07 +0800 Subject: [PATCH 58/70] =?UTF-8?q?refactor(cqrs):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E5=A4=84=E7=90=86=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CqrsDispatcher 中添加 Concurrent 包引用以支持线程安全操作 - 在全局引用文件中增加 Concurrent 包引用,统一并发编程支持 - 为后续的并发处理逻辑改进奠定基础架构支持 --- GFramework.Cqrs/GlobalUsings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs index b60938a5..3085d1e1 100644 --- a/GFramework.Cqrs/GlobalUsings.cs +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -6,3 +6,4 @@ global using System.Threading.Tasks; global using System.Reflection; global using Microsoft.Extensions.DependencyInjection; global using System.Diagnostics; +global using System.Collections.Concurrent; From 4951fb0254be4220725ce7c6a20970d2c971db89 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:19:44 +0800 Subject: [PATCH 59/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=88=86=E5=8F=91=E5=99=A8=E5=92=8C=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsDispatcher 类,支持请求/通知/流式请求的分发处理 - 添加进程级缓存机制,优化热路径中的反射和类型构造性能 - 实现上下文感知处理器的 CQRS 分发上下文注入功能 - 开发 CqrsHandlerRegistryGenerator 源代码生成器,减少运行时反射扫描 - 添加完整的单元测试验证缓存机制和服务类型注册功能 - 支持管道行为链处理和异步流式请求响应模式 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 68 +++- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 25 +- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 223 +++++++---- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 347 ++++++++++-------- 4 files changed, 429 insertions(+), 234 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 6c52a910..5ae794ad 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -12,9 +12,6 @@ namespace GFramework.Cqrs.Tests.Cqrs; [TestFixture] internal sealed class CqrsDispatcherCacheTests { - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 初始化测试上下文。 /// @@ -23,6 +20,7 @@ internal sealed class CqrsDispatcherCacheTests { LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); _container = new MicrosoftDiContainer(); + _container.RegisterCqrsPipelineBehavior(); CqrsTestRuntime.RegisterHandlers( _container, @@ -43,6 +41,9 @@ internal sealed class CqrsDispatcherCacheTests _container = null; } + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// /// 验证相同消息类型重复分发时,不会重复扩张服务类型缓存。 /// @@ -52,32 +53,54 @@ internal sealed class CqrsDispatcherCacheTests var notificationServiceTypes = GetCacheField("NotificationHandlerServiceTypes"); var requestServiceTypes = GetCacheField("RequestServiceTypes"); var streamServiceTypes = GetCacheField("StreamHandlerServiceTypes"); + var requestInvokers = GetCacheField("RequestInvokers"); + var requestPipelineInvokers = GetCacheField("RequestPipelineInvokers"); + var notificationInvokers = GetCacheField("NotificationInvokers"); + var streamInvokers = GetCacheField("StreamInvokers"); var notificationBefore = notificationServiceTypes.Count; var requestBefore = requestServiceTypes.Count; var streamBefore = streamServiceTypes.Count; + var requestInvokersBefore = requestInvokers.Count; + var requestPipelineInvokersBefore = requestPipelineInvokers.Count; + var notificationInvokersBefore = notificationInvokers.Count; + var streamInvokersBefore = streamInvokers.Count; await _context!.SendRequestAsync(new DispatcherCacheRequest()); + await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); await _context.PublishAsync(new DispatcherCacheNotification()); await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest())); var notificationAfterFirstDispatch = notificationServiceTypes.Count; var requestAfterFirstDispatch = requestServiceTypes.Count; var streamAfterFirstDispatch = streamServiceTypes.Count; + var requestInvokersAfterFirstDispatch = requestInvokers.Count; + var requestPipelineInvokersAfterFirstDispatch = requestPipelineInvokers.Count; + var notificationInvokersAfterFirstDispatch = notificationInvokers.Count; + var streamInvokersAfterFirstDispatch = streamInvokers.Count; await _context.SendRequestAsync(new DispatcherCacheRequest()); + await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); await _context.PublishAsync(new DispatcherCacheNotification()); await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest())); Assert.Multiple(() => { Assert.That(notificationAfterFirstDispatch, Is.EqualTo(notificationBefore + 1)); - Assert.That(requestAfterFirstDispatch, Is.EqualTo(requestBefore + 1)); + Assert.That(requestAfterFirstDispatch, Is.EqualTo(requestBefore + 2)); Assert.That(streamAfterFirstDispatch, Is.EqualTo(streamBefore + 1)); + Assert.That(requestInvokersAfterFirstDispatch, Is.EqualTo(requestInvokersBefore + 1)); + Assert.That(requestPipelineInvokersAfterFirstDispatch, Is.EqualTo(requestPipelineInvokersBefore + 1)); + Assert.That(notificationInvokersAfterFirstDispatch, Is.EqualTo(notificationInvokersBefore + 1)); + Assert.That(streamInvokersAfterFirstDispatch, Is.EqualTo(streamInvokersBefore + 1)); Assert.That(notificationServiceTypes.Count, Is.EqualTo(notificationAfterFirstDispatch)); Assert.That(requestServiceTypes.Count, Is.EqualTo(requestAfterFirstDispatch)); Assert.That(streamServiceTypes.Count, Is.EqualTo(streamAfterFirstDispatch)); + Assert.That(requestInvokers.Count, Is.EqualTo(requestInvokersAfterFirstDispatch)); + Assert.That(requestPipelineInvokers.Count, Is.EqualTo(requestPipelineInvokersAfterFirstDispatch)); + Assert.That(notificationInvokers.Count, Is.EqualTo(notificationInvokersAfterFirstDispatch)); + Assert.That(streamInvokers.Count, Is.EqualTo(streamInvokersAfterFirstDispatch)); }); } @@ -126,6 +149,11 @@ internal sealed record DispatcherCacheNotification : INotification; /// internal sealed record DispatcherCacheStreamRequest : IStreamRequest; +/// +/// 用于验证 pipeline invoker 缓存的测试请求。 +/// +internal sealed record DispatcherPipelineCacheRequest : IRequest; + /// /// 处理 。 /// @@ -170,3 +198,35 @@ internal sealed class DispatcherCacheStreamHandler : IStreamRequestHandler +/// 处理 。 +/// +internal sealed class DispatcherPipelineCacheRequestHandler : IRequestHandler +{ + /// + /// 返回固定结果,供 pipeline 缓存测试使用。 + /// + public ValueTask Handle(DispatcherPipelineCacheRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(2); + } +} + +/// +/// 为 提供最小 pipeline 行为, +/// 用于命中 dispatcher 的 pipeline invoker 缓存分支。 +/// +internal sealed class DispatcherPipelineCacheBehavior : IPipelineBehavior +{ + /// + /// 直接转发到下一个处理器。 + /// + public ValueTask Handle( + DispatcherPipelineCacheRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(request, cancellationToken); + } +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 91532e17..002b7edc 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -44,6 +44,19 @@ internal sealed class CqrsDispatcher( StreamHandlerServiceTypes = new(); + // 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。 + private static readonly MethodInfo RequestHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo RequestPipelineInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo NotificationHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo StreamHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeStreamHandler), BindingFlags.NonPublic | BindingFlags.Static)!; + /// /// 发布通知到所有已注册处理器。 /// @@ -189,8 +202,7 @@ internal sealed class CqrsDispatcher( /// private static RequestInvoker CreateRequestInvoker(Type requestType, Type responseType) { - var method = typeof(CqrsDispatcher) - .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)! + var method = RequestHandlerInvokerMethodDefinition .MakeGenericMethod(requestType, responseType); return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); } @@ -200,8 +212,7 @@ internal sealed class CqrsDispatcher( /// private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType, Type responseType) { - var method = typeof(CqrsDispatcher) - .GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)! + var method = RequestPipelineInvokerMethodDefinition .MakeGenericMethod(requestType, responseType); return (RequestPipelineInvoker)Delegate.CreateDelegate(typeof(RequestPipelineInvoker), method); } @@ -211,8 +222,7 @@ internal sealed class CqrsDispatcher( /// private static NotificationInvoker CreateNotificationInvoker(Type notificationType) { - var method = typeof(CqrsDispatcher) - .GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)! + var method = NotificationHandlerInvokerMethodDefinition .MakeGenericMethod(notificationType); return (NotificationInvoker)Delegate.CreateDelegate(typeof(NotificationInvoker), method); } @@ -222,8 +232,7 @@ internal sealed class CqrsDispatcher( /// private static StreamInvoker CreateStreamInvoker(Type requestType, Type responseType) { - var method = typeof(CqrsDispatcher) - .GetMethod(nameof(InvokeStreamHandler), BindingFlags.NonPublic | BindingFlags.Static)! + var method = StreamHandlerInvokerMethodDefinition .MakeGenericMethod(requestType, responseType); return (StreamInvoker)Delegate.CreateDelegate(typeof(StreamInvoker), method); } diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index e1ec1546..dcdb5e5f 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -10,6 +10,138 @@ namespace GFramework.SourceGenerators.Tests.Cqrs; [TestFixture] public class CqrsHandlerRegistryGeneratorTests { + private const string HiddenNestedHandlerSelfRegistrationExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + RegisterReflectedHandler(services, logger, registryAssembly, "TestApp.Container+HiddenHandler"); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + typeof(global::TestApp.VisibleHandler)); + logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + + private static void RegisterReflectedHandler(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Reflection.Assembly registryAssembly, string implementationTypeMetadataName) + { + var implementationType = registryAssembly.GetType(implementationTypeMetadataName, throwOnError: false, ignoreCase: false); + if (implementationType is null) + return; + + var handlerInterfaces = implementationType.GetInterfaces(); + global::System.Array.Sort(handlerInterfaces, CompareTypes); + + foreach (var handlerInterface in handlerInterfaces) + { + if (!IsSupportedHandlerInterface(handlerInterface)) + continue; + + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + handlerInterface, + implementationType); + logger.Debug($"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}."); + } + } + + private static int CompareTypes(global::System.Type left, global::System.Type right) + { + return global::System.StringComparer.Ordinal.Compare(GetRuntimeTypeDisplayName(left), GetRuntimeTypeDisplayName(right)); + } + + private static bool IsSupportedHandlerInterface(global::System.Type interfaceType) + { + if (!interfaceType.IsGenericType) + return false; + + var definitionFullName = interfaceType.GetGenericTypeDefinition().FullName; + return global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler`2") + || global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler`1") + || global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler`2"); + } + + private static string GetRuntimeTypeDisplayName(global::System.Type type) + { + if (type == typeof(string)) + return "string"; + if (type == typeof(int)) + return "int"; + if (type == typeof(long)) + return "long"; + if (type == typeof(short)) + return "short"; + if (type == typeof(byte)) + return "byte"; + if (type == typeof(bool)) + return "bool"; + if (type == typeof(object)) + return "object"; + if (type == typeof(void)) + return "void"; + if (type == typeof(uint)) + return "uint"; + if (type == typeof(ulong)) + return "ulong"; + if (type == typeof(ushort)) + return "ushort"; + if (type == typeof(sbyte)) + return "sbyte"; + if (type == typeof(float)) + return "float"; + if (type == typeof(double)) + return "double"; + if (type == typeof(decimal)) + return "decimal"; + if (type == typeof(char)) + return "char"; + + if (type.IsArray) + return GetRuntimeTypeDisplayName(type.GetElementType()!) + "[]"; + + if (!type.IsGenericType) + return (type.FullName ?? type.Name).Replace('+', '.'); + + var genericTypeName = type.GetGenericTypeDefinition().FullName ?? type.Name; + var arityIndex = genericTypeName.IndexOf('`'); + if (arityIndex >= 0) + genericTypeName = genericTypeName[..arityIndex]; + + genericTypeName = genericTypeName.Replace('+', '.'); + var arguments = type.GetGenericArguments(); + var builder = new global::System.Text.StringBuilder(); + builder.Append(genericTypeName); + builder.Append('<'); + + for (var index = 0; index < arguments.Length; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append(GetRuntimeTypeDisplayName(arguments[index])); + } + + builder.Append('>'); + return builder.ToString(); + } + } + + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -126,12 +258,12 @@ public class CqrsHandlerRegistryGeneratorTests } /// - /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器, - /// 并额外标记运行时补充反射扫描。 + /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册, + /// 不再依赖程序集级 fallback marker。 /// [Test] public async Task - Generates_Visible_Handlers_And_Requests_Reflection_Fallback_When_Assembly_Contains_Private_Nested_Handler() + Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_Handler() { const string source = """ using System; @@ -202,45 +334,17 @@ public class CqrsHandlerRegistryGeneratorTests } """; - const string expected = """ - // - #nullable enable - - [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] - [assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute("TestApp.Container+HiddenHandler")] - - namespace GFramework.Generated.Cqrs; - - internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry - { - public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) - { - if (services is null) - throw new global::System.ArgumentNullException(nameof(services)); - if (logger is null) - throw new global::System.ArgumentNullException(nameof(logger)); - - global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( - services, - typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), - typeof(global::TestApp.VisibleHandler)); - logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); - } - } - - """; - await GeneratorTest.RunAsync( source, - ("CqrsHandlerRegistry.g.cs", expected)); + ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } /// - /// 验证当 runtime 仅支持旧版无参 fallback marker 时,生成器会退回旧语义, - /// 只输出 marker 而不输出精确类型名。 + /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, + /// 不再输出 fallback marker。 /// [Test] - public async Task Generates_Legacy_Fallback_Marker_When_Runtime_Does_Not_Support_Type_Name_List() + public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_Handler() { const string source = """ using System; @@ -311,45 +415,17 @@ public class CqrsHandlerRegistryGeneratorTests } """; - const string expected = """ - // - #nullable enable - - [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] - [assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute()] - - namespace GFramework.Generated.Cqrs; - - internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry - { - public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) - { - if (services is null) - throw new global::System.ArgumentNullException(nameof(services)); - if (logger is null) - throw new global::System.ArgumentNullException(nameof(logger)); - - global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( - services, - typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), - typeof(global::TestApp.VisibleHandler)); - logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); - } - } - - """; - await GeneratorTest.RunAsync( source, - ("CqrsHandlerRegistry.g.cs", expected)); + ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } /// - /// 验证当旧版 runtime 合同中不存在 reflection fallback 标记特性时, - /// 生成器会保留此前的整程序集回退行为,避免丢失不可见 handlers。 + /// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性, + /// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。 /// [Test] - public async Task Skips_Generation_For_Unsupported_Handler_When_Fallback_Marker_Is_Unavailable() + public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable() { const string source = """ using System; @@ -414,16 +490,9 @@ public class CqrsHandlerRegistryGeneratorTests } """; - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; - - await test.RunAsync(); + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } /// diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 1e260e32..83559781 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -16,9 +16,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; - private const string CqrsReflectionFallbackAttributeMetadataName = - $"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute"; - private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; @@ -57,10 +54,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; - return new GenerationEnvironment( - generationEnabled, - GetReflectionFallbackEmissionMode( - compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName))); + return new GenerationEnvironment(generationEnabled); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -91,17 +85,20 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return null; var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var implementationLogName = GetLogDisplayName(type); if (!CanReferenceFromGeneratedRegistry(type) || handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) { + // Non-public handlers and handlers closed over non-public message types cannot appear in typeof(...) + // expressions inside generated code. Preserve generator hit rate by resolving just that implementation + // type back from the current assembly instead of asking the runtime registrar to rescan the assembly. return new HandlerCandidateAnalysis( implementationTypeDisplayName, + implementationLogName, ImmutableArray.Empty, - true, - GetReflectionFallbackTypeName(type)); + GetReflectionTypeMetadataName(type)); } - var implementationLogName = GetLogDisplayName(type); var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); foreach (var handlerInterface in handlerInterfaces) { @@ -114,8 +111,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return new HandlerCandidateAnalysis( implementationTypeDisplayName, + implementationLogName, registrations.MoveToImmutable(), - false, null); } @@ -125,40 +122,23 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (!generationEnvironment.GenerationEnabled) return; - var registrations = CollectRegistrations( - candidates, - out var hasUnsupportedConcreteHandler, - out var reflectionFallbackTypeNames); + var registrations = CollectRegistrations(candidates); if (registrations.Count == 0) return; - // If the runtime contract does not yet expose the reflection fallback marker, - // keep the previous all-or-nothing behavior so unsupported handlers are not silently dropped. - if (hasUnsupportedConcreteHandler && - generationEnvironment.ReflectionFallbackEmissionMode == ReflectionFallbackEmissionMode.Disabled) - return; - context.AddSource( HintName, - GenerateSource( - registrations, - hasUnsupportedConcreteHandler, - generationEnvironment.ReflectionFallbackEmissionMode, - reflectionFallbackTypeNames)); + GenerateSource(registrations)); } - private static List CollectRegistrations( - ImmutableArray candidates, - out bool hasUnsupportedConcreteHandler, - out IReadOnlyList reflectionFallbackTypeNames) + private static List CollectRegistrations( + ImmutableArray candidates) { - var registrations = new List(); - hasUnsupportedConcreteHandler = false; - var fallbackTypeNames = new SortedSet(StringComparer.Ordinal); + var registrations = new List(); // Partial declarations surface the same symbol through multiple syntax nodes. - // Collapse them by implementation type so generated registrations stay stable and duplicate-free. + // Collapse them by implementation type so direct and reflected registrations stay stable and duplicate-free. var uniqueCandidates = new Dictionary(StringComparer.Ordinal); foreach (var candidate in candidates) @@ -166,25 +146,16 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (candidate is null) continue; - if (candidate.Value.HasUnsupportedConcreteHandler) - { - hasUnsupportedConcreteHandler = true; - var reflectionFallbackTypeName = candidate.Value.ReflectionFallbackTypeName; - if (reflectionFallbackTypeName is not null && - !string.IsNullOrWhiteSpace(reflectionFallbackTypeName)) - { - fallbackTypeNames.Add(reflectionFallbackTypeName); - } - - continue; - } - uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value; } foreach (var candidate in uniqueCandidates.Values) { - registrations.AddRange(candidate.Registrations); + registrations.Add(new ImplementationRegistrationSpec( + candidate.ImplementationTypeDisplayName, + candidate.ImplementationLogName, + candidate.Registrations, + candidate.ReflectionTypeMetadataName)); } registrations.Sort(static (left, right) => @@ -193,35 +164,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator left.ImplementationLogName, right.ImplementationLogName); - return implementationComparison != 0 - ? implementationComparison - : StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName); + return implementationComparison; }); - reflectionFallbackTypeNames = fallbackTypeNames.ToArray(); return registrations; } - private static ReflectionFallbackEmissionMode GetReflectionFallbackEmissionMode(INamedTypeSymbol? attributeType) - { - if (attributeType is null) - return ReflectionFallbackEmissionMode.Disabled; - - foreach (var constructor in attributeType.InstanceConstructors) - { - if (constructor.Parameters.Length != 1) - continue; - - if (constructor.Parameters[0].Type is IArrayTypeSymbol arrayType && - arrayType.ElementType.SpecialType == SpecialType.System_String) - { - return ReflectionFallbackEmissionMode.PreciseTypeNames; - } - } - - return ReflectionFallbackEmissionMode.MarkerOnly; - } - private static bool IsConcreteHandlerType(INamedTypeSymbol type) { return type.TypeKind is TypeKind.Class or TypeKind.Struct && @@ -313,7 +261,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return builder.ToString(); } - private static string GetReflectionFallbackTypeName(INamedTypeSymbol type) + private static string GetReflectionTypeMetadataName(INamedTypeSymbol type) { var nestedTypes = new Stack(); for (var current = type; current is not null; current = current.ContainingType) @@ -352,11 +300,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } private static string GenerateSource( - IReadOnlyList registrations, - bool emitReflectionFallbackAttribute, - ReflectionFallbackEmissionMode reflectionFallbackEmissionMode, - IReadOnlyList reflectionFallbackTypeNames) + IReadOnlyList registrations) { + var hasReflectionRegistrations = registrations.Any(static registration => + !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)); var builder = new StringBuilder(); builder.AppendLine("// "); builder.AppendLine("#nullable enable"); @@ -368,11 +315,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append('.'); builder.Append(GeneratedTypeName); builder.AppendLine("))]"); - if (emitReflectionFallbackAttribute && - reflectionFallbackEmissionMode != ReflectionFallbackEmissionMode.Disabled) - { - AppendReflectionFallbackAttribute(builder, reflectionFallbackEmissionMode, reflectionFallbackTypeNames); - } builder.AppendLine(); builder.Append("namespace "); @@ -394,59 +336,177 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); builder.AppendLine(" if (logger is null)"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); - builder.AppendLine(); + if (hasReflectionRegistrations) + { + builder.AppendLine(); + builder.Append(" var registryAssembly = typeof(global::"); + builder.Append(GeneratedNamespace); + builder.Append('.'); + builder.Append(GeneratedTypeName); + builder.AppendLine(").Assembly;"); + } + + if (registrations.Count > 0) + builder.AppendLine(); foreach (var registration in registrations) { - builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.Append(" typeof("); - builder.Append(registration.HandlerInterfaceDisplayName); - builder.AppendLine("),"); - builder.Append(" typeof("); - builder.Append(registration.ImplementationTypeDisplayName); - builder.AppendLine("));"); - builder.Append(" logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(registration.HandlerInterfaceLogName)); - builder.AppendLine(".\");"); + if (!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) + { + AppendReflectionRegistration(builder, registration.ReflectionTypeMetadataName!); + continue; + } + + foreach (var directRegistration in registration.DirectRegistrations) + { + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" typeof("); + builder.Append(directRegistration.ImplementationTypeDisplayName); + builder.AppendLine("));"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(directRegistration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + } } builder.AppendLine(" }"); + + if (hasReflectionRegistrations) + { + builder.AppendLine(); + AppendReflectionHelpers(builder); + } + builder.AppendLine("}"); return builder.ToString(); } - private static void AppendReflectionFallbackAttribute( - StringBuilder builder, - ReflectionFallbackEmissionMode reflectionFallbackEmissionMode, - IReadOnlyList reflectionFallbackTypeNames) + private static void AppendReflectionRegistration(StringBuilder builder, string reflectionTypeMetadataName) { - builder.Append("[assembly: global::"); - builder.Append(CqrsRuntimeNamespace); - builder.Append(".CqrsReflectionFallbackAttribute"); + builder.Append(" RegisterReflectedHandler(services, logger, registryAssembly, \""); + builder.Append(EscapeStringLiteral(reflectionTypeMetadataName)); + builder.AppendLine("\");"); + } - if (reflectionFallbackEmissionMode == ReflectionFallbackEmissionMode.PreciseTypeNames && - reflectionFallbackTypeNames.Count > 0) - { - builder.Append('('); - for (var index = 0; index < reflectionFallbackTypeNames.Count; index++) - { - if (index > 0) - builder.Append(", "); - - builder.Append('"'); - builder.Append(EscapeStringLiteral(reflectionFallbackTypeNames[index])); - builder.Append('"'); - } - - builder.AppendLine(")]"); - return; - } - - builder.AppendLine("()]"); + private static void AppendReflectionHelpers(StringBuilder builder) + { + // Emit the runtime helper methods only when at least one handler requires metadata-name lookup. + builder.AppendLine( + " private static void RegisterReflectedHandler(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Reflection.Assembly registryAssembly, string implementationTypeMetadataName)"); + builder.AppendLine(" {"); + builder.AppendLine( + " var implementationType = registryAssembly.GetType(implementationTypeMetadataName, throwOnError: false, ignoreCase: false);"); + builder.AppendLine(" if (implementationType is null)"); + builder.AppendLine(" return;"); + builder.AppendLine(); + builder.AppendLine(" var handlerInterfaces = implementationType.GetInterfaces();"); + builder.AppendLine(" global::System.Array.Sort(handlerInterfaces, CompareTypes);"); + builder.AppendLine(); + builder.AppendLine(" foreach (var handlerInterface in handlerInterfaces)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (!IsSupportedHandlerInterface(handlerInterface))"); + builder.AppendLine(" continue;"); + builder.AppendLine(); + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.AppendLine(" handlerInterface,"); + builder.AppendLine(" implementationType);"); + builder.AppendLine( + " logger.Debug($\"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}.\");"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static int CompareTypes(global::System.Type left, global::System.Type right)"); + builder.AppendLine(" {"); + builder.AppendLine( + " return global::System.StringComparer.Ordinal.Compare(GetRuntimeTypeDisplayName(left), GetRuntimeTypeDisplayName(right));"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static bool IsSupportedHandlerInterface(global::System.Type interfaceType)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (!interfaceType.IsGenericType)"); + builder.AppendLine(" return false;"); + builder.AppendLine(); + builder.AppendLine(" var definitionFullName = interfaceType.GetGenericTypeDefinition().FullName;"); + builder.AppendLine( + $" return global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{IRequestHandlerMetadataName}\")"); + builder.AppendLine( + $" || global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{INotificationHandlerMetadataName}\")"); + builder.AppendLine( + $" || global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{IStreamRequestHandlerMetadataName}\");"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static string GetRuntimeTypeDisplayName(global::System.Type type)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (type == typeof(string))"); + builder.AppendLine(" return \"string\";"); + builder.AppendLine(" if (type == typeof(int))"); + builder.AppendLine(" return \"int\";"); + builder.AppendLine(" if (type == typeof(long))"); + builder.AppendLine(" return \"long\";"); + builder.AppendLine(" if (type == typeof(short))"); + builder.AppendLine(" return \"short\";"); + builder.AppendLine(" if (type == typeof(byte))"); + builder.AppendLine(" return \"byte\";"); + builder.AppendLine(" if (type == typeof(bool))"); + builder.AppendLine(" return \"bool\";"); + builder.AppendLine(" if (type == typeof(object))"); + builder.AppendLine(" return \"object\";"); + builder.AppendLine(" if (type == typeof(void))"); + builder.AppendLine(" return \"void\";"); + builder.AppendLine(" if (type == typeof(uint))"); + builder.AppendLine(" return \"uint\";"); + builder.AppendLine(" if (type == typeof(ulong))"); + builder.AppendLine(" return \"ulong\";"); + builder.AppendLine(" if (type == typeof(ushort))"); + builder.AppendLine(" return \"ushort\";"); + builder.AppendLine(" if (type == typeof(sbyte))"); + builder.AppendLine(" return \"sbyte\";"); + builder.AppendLine(" if (type == typeof(float))"); + builder.AppendLine(" return \"float\";"); + builder.AppendLine(" if (type == typeof(double))"); + builder.AppendLine(" return \"double\";"); + builder.AppendLine(" if (type == typeof(decimal))"); + builder.AppendLine(" return \"decimal\";"); + builder.AppendLine(" if (type == typeof(char))"); + builder.AppendLine(" return \"char\";"); + builder.AppendLine(); + builder.AppendLine(" if (type.IsArray)"); + builder.AppendLine(" return GetRuntimeTypeDisplayName(type.GetElementType()!) + \"[]\";"); + builder.AppendLine(); + builder.AppendLine(" if (!type.IsGenericType)"); + builder.AppendLine(" return (type.FullName ?? type.Name).Replace('+', '.');"); + builder.AppendLine(); + builder.AppendLine(" var genericTypeName = type.GetGenericTypeDefinition().FullName ?? type.Name;"); + builder.AppendLine(" var arityIndex = genericTypeName.IndexOf('`');"); + builder.AppendLine(" if (arityIndex >= 0)"); + builder.AppendLine(" genericTypeName = genericTypeName[..arityIndex];"); + builder.AppendLine(); + builder.AppendLine(" genericTypeName = genericTypeName.Replace('+', '.');"); + builder.AppendLine(" var arguments = type.GetGenericArguments();"); + builder.AppendLine(" var builder = new global::System.Text.StringBuilder();"); + builder.AppendLine(" builder.Append(genericTypeName);"); + builder.AppendLine(" builder.Append('<');"); + builder.AppendLine(); + builder.AppendLine(" for (var index = 0; index < arguments.Length; index++)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (index > 0)"); + builder.AppendLine(" builder.Append(\", \");"); + builder.AppendLine(); + builder.AppendLine(" builder.Append(GetRuntimeTypeDisplayName(arguments[index]));"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" builder.Append('>');"); + builder.AppendLine(" return builder.ToString();"); + builder.AppendLine(" }"); } private static string EscapeStringLiteral(string value) @@ -463,34 +523,40 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string HandlerInterfaceLogName, string ImplementationLogName); + private readonly record struct ImplementationRegistrationSpec( + string ImplementationTypeDisplayName, + string ImplementationLogName, + ImmutableArray DirectRegistrations, + string? ReflectionTypeMetadataName); + private readonly struct HandlerCandidateAnalysis : IEquatable { public HandlerCandidateAnalysis( string implementationTypeDisplayName, + string implementationLogName, ImmutableArray registrations, - bool hasUnsupportedConcreteHandler, - string? reflectionFallbackTypeName) + string? reflectionTypeMetadataName) { ImplementationTypeDisplayName = implementationTypeDisplayName; + ImplementationLogName = implementationLogName; Registrations = registrations; - HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler; - ReflectionFallbackTypeName = reflectionFallbackTypeName; + ReflectionTypeMetadataName = reflectionTypeMetadataName; } public string ImplementationTypeDisplayName { get; } + public string ImplementationLogName { get; } + public ImmutableArray Registrations { get; } - public bool HasUnsupportedConcreteHandler { get; } - - public string? ReflectionFallbackTypeName { get; } + public string? ReflectionTypeMetadataName { get; } public bool Equals(HandlerCandidateAnalysis other) { if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, StringComparison.Ordinal) || - HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler || - !string.Equals(ReflectionFallbackTypeName, other.ReflectionFallbackTypeName, + !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || + !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, StringComparison.Ordinal) || Registrations.Length != other.Registrations.Length) { @@ -516,11 +582,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator unchecked { var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName); - hashCode = (hashCode * 397) ^ HasUnsupportedConcreteHandler.GetHashCode(); + hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName); hashCode = (hashCode * 397) ^ - (ReflectionFallbackTypeName is null + (ReflectionTypeMetadataName is null ? 0 - : StringComparer.Ordinal.GetHashCode(ReflectionFallbackTypeName)); + : StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName)); foreach (var registration in Registrations) { hashCode = (hashCode * 397) ^ registration.GetHashCode(); @@ -531,14 +597,5 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } } - private readonly record struct GenerationEnvironment( - bool GenerationEnabled, - ReflectionFallbackEmissionMode ReflectionFallbackEmissionMode); - - private enum ReflectionFallbackEmissionMode - { - Disabled, - MarkerOnly, - PreciseTypeNames - } + private readonly record struct GenerationEnvironment(bool GenerationEnabled); } From 76bb9671d5232034961d478af85163852b721847 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:50:02 +0800 Subject: [PATCH 60/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsHandlerRegistryGenerator 源代码生成器 - 为 CQRS 请求处理器、通知处理器和流请求处理器生成注册代码 - 减少运行时程序集反射扫描开销 - 支持直接类型引用和反射加载两种注册方式 - 提供完整的单元测试验证生成器功能 - 实现对私有嵌套类型的反射注册支持 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 105 +++++++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 126 +++++++++++++++--- 2 files changed, 210 insertions(+), 21 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index dcdb5e5f..679fd0fa 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -142,6 +142,39 @@ public class CqrsHandlerRegistryGeneratorTests """; + private const string HiddenImplementationDirectInterfaceRegistrationExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -339,6 +372,78 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } + /// + /// 验证当隐藏实现类型的 handler 接口仍可被生成代码直接引用时, + /// 生成器只会定向反射实现类型,而不会再生成基于 GetInterfaces() 的接口发现辅助逻辑。 + /// + [Test] + public async Task + Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [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() : IRequest; + + public sealed class Container + { + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenImplementationDirectInterfaceRegistrationExpected)); + } + /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 83559781..27b01f9f 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -86,22 +86,35 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var implementationLogName = GetLogDisplayName(type); - if (!CanReferenceFromGeneratedRegistry(type) || - handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType))) - { - // Non-public handlers and handlers closed over non-public message types cannot appear in typeof(...) - // expressions inside generated code. Preserve generator hit rate by resolving just that implementation - // type back from the current assembly instead of asking the runtime registrar to rescan the assembly. - return new HandlerCandidateAnalysis( - implementationTypeDisplayName, - implementationLogName, - ImmutableArray.Empty, - GetReflectionTypeMetadataName(type)); - } - + var canReferenceImplementation = CanReferenceFromGeneratedRegistry(type); var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var reflectedImplementationRegistrations = + ImmutableArray.CreateBuilder(handlerInterfaces.Length); foreach (var handlerInterface in handlerInterfaces) { + var canReferenceHandlerInterface = CanReferenceFromGeneratedRegistry(handlerInterface); + if (!canReferenceImplementation || !canReferenceHandlerInterface) + { + if (!canReferenceImplementation && canReferenceHandlerInterface) + { + reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + GetLogDisplayName(handlerInterface))); + continue; + } + + // Non-public handlers closed over non-public message types still cannot be expressed purely as + // typeof(...) registrations. Preserve generator hit rate by resolving only the affected + // implementation back from the current assembly instead of asking the runtime registrar to rescan + // the whole assembly. + return new HandlerCandidateAnalysis( + implementationTypeDisplayName, + implementationLogName, + ImmutableArray.Empty, + ImmutableArray.Empty, + GetReflectionTypeMetadataName(type)); + } + registrations.Add(new HandlerRegistrationSpec( handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), implementationTypeDisplayName, @@ -112,8 +125,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return new HandlerCandidateAnalysis( implementationTypeDisplayName, implementationLogName, - registrations.MoveToImmutable(), - null); + registrations.ToImmutable(), + reflectedImplementationRegistrations.ToImmutable(), + canReferenceImplementation ? null : GetReflectionTypeMetadataName(type)); } private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, @@ -155,6 +169,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator candidate.ImplementationTypeDisplayName, candidate.ImplementationLogName, candidate.Registrations, + candidate.ReflectedImplementationRegistrations, candidate.ReflectionTypeMetadataName)); } @@ -302,8 +317,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private static string GenerateSource( IReadOnlyList registrations) { - var hasReflectionRegistrations = registrations.Any(static registration => - !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)); + var hasReflectedImplementationRegistrations = registrations.Any(static registration => + !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); + var hasFullReflectionRegistrations = registrations.Any(static registration => + !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName) && + registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); var builder = new StringBuilder(); builder.AppendLine("// "); builder.AppendLine("#nullable enable"); @@ -336,7 +354,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); builder.AppendLine(" if (logger is null)"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); - if (hasReflectionRegistrations) + if (hasReflectedImplementationRegistrations || hasFullReflectionRegistrations) { builder.AppendLine(); builder.Append(" var registryAssembly = typeof(global::"); @@ -349,8 +367,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (registrations.Count > 0) builder.AppendLine(); - foreach (var registration in registrations) + for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++) { + var registration = registrations[registrationIndex]; + if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty) + { + AppendReflectedImplementationRegistrations(builder, registration, registrationIndex); + continue; + } + if (!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) { AppendReflectionRegistration(builder, registration.ReflectionTypeMetadataName!); @@ -378,7 +403,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" }"); - if (hasReflectionRegistrations) + if (hasFullReflectionRegistrations) { builder.AppendLine(); AppendReflectionHelpers(builder); @@ -395,6 +420,43 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine("\");"); } + private static void AppendReflectedImplementationRegistrations( + StringBuilder builder, + ImplementationRegistrationSpec registration, + int registrationIndex) + { + var implementationVariableName = $"implementationType{registrationIndex}"; + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + builder.Append(" if ("); + builder.Append(implementationVariableName); + builder.AppendLine(" is not null)"); + builder.AppendLine(" {"); + + foreach (var reflectedRegistration in registration.ReflectedImplementationRegistrations) + { + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(reflectedRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + } + + builder.AppendLine(" }"); + } + private static void AppendReflectionHelpers(StringBuilder builder) { // Emit the runtime helper methods only when at least one handler requires metadata-name lookup. @@ -523,10 +585,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string HandlerInterfaceLogName, string ImplementationLogName); + private readonly record struct ReflectedImplementationRegistrationSpec( + string HandlerInterfaceDisplayName, + string HandlerInterfaceLogName); + private readonly record struct ImplementationRegistrationSpec( string ImplementationTypeDisplayName, string ImplementationLogName, ImmutableArray DirectRegistrations, + ImmutableArray ReflectedImplementationRegistrations, string? ReflectionTypeMetadataName); private readonly struct HandlerCandidateAnalysis : IEquatable @@ -535,11 +602,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string implementationTypeDisplayName, string implementationLogName, ImmutableArray registrations, + ImmutableArray reflectedImplementationRegistrations, string? reflectionTypeMetadataName) { ImplementationTypeDisplayName = implementationTypeDisplayName; ImplementationLogName = implementationLogName; Registrations = registrations; + ReflectedImplementationRegistrations = reflectedImplementationRegistrations; ReflectionTypeMetadataName = reflectionTypeMetadataName; } @@ -549,6 +618,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public ImmutableArray Registrations { get; } + public ImmutableArray ReflectedImplementationRegistrations { get; } + public string? ReflectionTypeMetadataName { get; } public bool Equals(HandlerCandidateAnalysis other) @@ -558,7 +629,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, StringComparison.Ordinal) || - Registrations.Length != other.Registrations.Length) + Registrations.Length != other.Registrations.Length || + ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length) { return false; } @@ -569,6 +641,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } + for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++) + { + if (!ReflectedImplementationRegistrations[index].Equals( + other.ReflectedImplementationRegistrations[index])) + return false; + } + return true; } @@ -592,6 +671,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator hashCode = (hashCode * 397) ^ registration.GetHashCode(); } + foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations) + { + hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode(); + } + return hashCode; } } From f25353db8c763694a2834ba4832131a3ccca2a92 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:02:01 +0800 Subject: [PATCH 61/70] =?UTF-8?q?feat(cqrs-generator):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=B3=9B=E5=9E=8B=E4=B8=8E=E6=95=B0=E7=BB=84=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E9=87=8D=E5=BB=BA=E5=B9=B6=E4=BC=98=E5=8C=96=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E5=A4=84=E7=90=86=E5=99=A8=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 341 +++++++++----- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 429 +++++++++++++++++- 2 files changed, 642 insertions(+), 128 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 679fd0fa..ef70ec4a 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -29,115 +29,26 @@ public class CqrsHandlerRegistryGeneratorTests var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; - RegisterReflectedHandler(services, logger, registryAssembly, "TestApp.Container+HiddenHandler"); + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, typeof(string)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( services, typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(global::TestApp.VisibleHandler)); logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); } - - private static void RegisterReflectedHandler(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Reflection.Assembly registryAssembly, string implementationTypeMetadataName) - { - var implementationType = registryAssembly.GetType(implementationTypeMetadataName, throwOnError: false, ignoreCase: false); - if (implementationType is null) - return; - - var handlerInterfaces = implementationType.GetInterfaces(); - global::System.Array.Sort(handlerInterfaces, CompareTypes); - - foreach (var handlerInterface in handlerInterfaces) - { - if (!IsSupportedHandlerInterface(handlerInterface)) - continue; - - global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( - services, - handlerInterface, - implementationType); - logger.Debug($"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}."); - } - } - - private static int CompareTypes(global::System.Type left, global::System.Type right) - { - return global::System.StringComparer.Ordinal.Compare(GetRuntimeTypeDisplayName(left), GetRuntimeTypeDisplayName(right)); - } - - private static bool IsSupportedHandlerInterface(global::System.Type interfaceType) - { - if (!interfaceType.IsGenericType) - return false; - - var definitionFullName = interfaceType.GetGenericTypeDefinition().FullName; - return global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler`2") - || global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler`1") - || global::System.StringComparer.Ordinal.Equals(definitionFullName, "GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler`2"); - } - - private static string GetRuntimeTypeDisplayName(global::System.Type type) - { - if (type == typeof(string)) - return "string"; - if (type == typeof(int)) - return "int"; - if (type == typeof(long)) - return "long"; - if (type == typeof(short)) - return "short"; - if (type == typeof(byte)) - return "byte"; - if (type == typeof(bool)) - return "bool"; - if (type == typeof(object)) - return "object"; - if (type == typeof(void)) - return "void"; - if (type == typeof(uint)) - return "uint"; - if (type == typeof(ulong)) - return "ulong"; - if (type == typeof(ushort)) - return "ushort"; - if (type == typeof(sbyte)) - return "sbyte"; - if (type == typeof(float)) - return "float"; - if (type == typeof(double)) - return "double"; - if (type == typeof(decimal)) - return "decimal"; - if (type == typeof(char)) - return "char"; - - if (type.IsArray) - return GetRuntimeTypeDisplayName(type.GetElementType()!) + "[]"; - - if (!type.IsGenericType) - return (type.FullName ?? type.Name).Replace('+', '.'); - - var genericTypeName = type.GetGenericTypeDefinition().FullName ?? type.Name; - var arityIndex = genericTypeName.IndexOf('`'); - if (arityIndex >= 0) - genericTypeName = genericTypeName[..arityIndex]; - - genericTypeName = genericTypeName.Replace('+', '.'); - var arguments = type.GetGenericArguments(); - var builder = new global::System.Text.StringBuilder(); - builder.Append(genericTypeName); - builder.Append('<'); - - for (var index = 0; index < arguments.Length; index++) - { - if (index > 0) - builder.Append(", "); - - builder.Append(GetRuntimeTypeDisplayName(arguments[index])); - } - - builder.Append('>'); - return builder.ToString(); - } } """; @@ -175,6 +86,84 @@ public class CqrsHandlerRegistryGeneratorTests """; + private const string HiddenArrayResponseFallbackExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1Element = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1Element is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType()); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + } + + """; + + private const string HiddenGenericEnvelopeResponseExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1GenericDefinition = registryAssembly.GetType("TestApp.Container+HiddenEnvelope`1", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1GenericDefinition is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1GenericDefinition.MakeGenericType(typeof(string))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler>."); + } + } + } + } + + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -444,6 +433,152 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", HiddenImplementationDirectInterfaceRegistrationExpected)); } + /// + /// 验证精确重建路径会递归覆盖隐藏元素类型数组, + /// 使这类 handler interface 也能直接生成 closed service type,而不再退回 GetInterfaces()。 + /// + [Test] + public async Task Generates_Precise_Service_Type_For_Hidden_Array_Type_Arguments() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenArrayResponseFallbackExpected)); + } + + /// + /// 验证精确重建路径会递归覆盖隐藏泛型定义, + /// 使“隐藏泛型定义 + 可见/常量型实参”的闭包类型也能直接生成 closed service type。 + /// + [Test] + public async Task Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed class HiddenEnvelope { } + + private sealed record HiddenRequest() : IRequest>; + + private sealed class HiddenHandler : IRequestHandler> { } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected)); + } + /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 27b01f9f..1119a92c 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -90,36 +90,48 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); var reflectedImplementationRegistrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var preciseReflectedRegistrations = + ImmutableArray.CreateBuilder(handlerInterfaces.Length); foreach (var handlerInterface in handlerInterfaces) { var canReferenceHandlerInterface = CanReferenceFromGeneratedRegistry(handlerInterface); - if (!canReferenceImplementation || !canReferenceHandlerInterface) + if (canReferenceImplementation && canReferenceHandlerInterface) { - if (!canReferenceImplementation && canReferenceHandlerInterface) - { - reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( - handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - GetLogDisplayName(handlerInterface))); - continue; - } - - // Non-public handlers closed over non-public message types still cannot be expressed purely as - // typeof(...) registrations. Preserve generator hit rate by resolving only the affected - // implementation back from the current assembly instead of asking the runtime registrar to rescan - // the whole assembly. - return new HandlerCandidateAnalysis( + registrations.Add(new HandlerRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), implementationTypeDisplayName, - implementationLogName, - ImmutableArray.Empty, - ImmutableArray.Empty, - GetReflectionTypeMetadataName(type)); + GetLogDisplayName(handlerInterface), + implementationLogName)); + continue; } - registrations.Add(new HandlerRegistrationSpec( - handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + if (!canReferenceImplementation && canReferenceHandlerInterface) + { + reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + GetLogDisplayName(handlerInterface))); + continue; + } + + if (TryCreatePreciseReflectedRegistration( + context.SemanticModel.Compilation, + handlerInterface, + out var preciseReflectedRegistration)) + { + preciseReflectedRegistrations.Add(preciseReflectedRegistration); + continue; + } + + // Some closed handler interfaces still contain runtime-only type shapes such as arrays closed over + // non-public element types. For those rare cases keep the narrow implementation lookup, but let the + // generated registry discover the exact supported interfaces from the implementation type at runtime. + return new HandlerCandidateAnalysis( implementationTypeDisplayName, - GetLogDisplayName(handlerInterface), - implementationLogName)); + implementationLogName, + ImmutableArray.Empty, + ImmutableArray.Empty, + ImmutableArray.Empty, + GetReflectionTypeMetadataName(type)); } return new HandlerCandidateAnalysis( @@ -127,6 +139,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator implementationLogName, registrations.ToImmutable(), reflectedImplementationRegistrations.ToImmutable(), + preciseReflectedRegistrations.ToImmutable(), canReferenceImplementation ? null : GetReflectionTypeMetadataName(type)); } @@ -170,6 +183,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator candidate.ImplementationLogName, candidate.Registrations, candidate.ReflectedImplementationRegistrations, + candidate.PreciseReflectedRegistrations, candidate.ReflectionTypeMetadataName)); } @@ -214,6 +228,114 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal); } + private static bool TryCreatePreciseReflectedRegistration( + Compilation compilation, + INamedTypeSymbol handlerInterface, + out PreciseReflectedRegistrationSpec registration) + { + var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition + .ConstructUnboundGenericType() + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var typeArguments = ImmutableArray.CreateBuilder(handlerInterface.TypeArguments.Length); + foreach (var typeArgument in handlerInterface.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference)) + { + registration = default; + return false; + } + + typeArguments.Add(runtimeTypeReference!); + } + + registration = new PreciseReflectedRegistrationSpec( + openHandlerTypeDisplayName, + GetLogDisplayName(handlerInterface), + typeArguments.ToImmutable()); + return true; + } + + private static bool TryCreateRuntimeTypeReference( + Compilation compilation, + ITypeSymbol type, + out RuntimeTypeReferenceSpec? runtimeTypeReference) + { + if (CanReferenceFromGeneratedRegistry(type)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( + type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + + if (type is IArrayTypeSymbol arrayType && + TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank); + return true; + } + + if (type is INamedTypeSymbol genericNamedType && + genericNamedType.IsGenericType && + !genericNamedType.IsUnboundGenericType && + TryCreateGenericTypeDefinitionReference(compilation, genericNamedType, out var genericTypeDefinitionReference)) + { + var genericTypeArguments = + ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); + foreach (var typeArgument in genericNamedType.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference)) + { + runtimeTypeReference = null; + return false; + } + + genericTypeArguments.Add(genericTypeArgumentReference!); + } + + runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric( + genericTypeDefinitionReference!, + genericTypeArguments.ToImmutable()); + return true; + } + + if (type is INamedTypeSymbol namedType && + SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup( + GetReflectionTypeMetadataName(namedType)); + return true; + } + + runtimeTypeReference = null; + return false; + } + + private static bool TryCreateGenericTypeDefinitionReference( + Compilation compilation, + INamedTypeSymbol genericNamedType, + out RuntimeTypeReferenceSpec? genericTypeDefinitionReference) + { + var genericTypeDefinition = genericNamedType.OriginalDefinition; + if (CanReferenceFromGeneratedRegistry(genericTypeDefinition)) + { + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference( + genericTypeDefinition + .ConstructUnboundGenericType() + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + + if (SymbolEqualityComparer.Default.Equals(genericTypeDefinition.ContainingAssembly, compilation.Assembly)) + { + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromReflectionLookup( + GetReflectionTypeMetadataName(genericTypeDefinition)); + return true; + } + + genericTypeDefinitionReference = null; + return false; + } + private static bool CanReferenceFromGeneratedRegistry(ITypeSymbol type) { switch (type) @@ -319,9 +441,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { var hasReflectedImplementationRegistrations = registrations.Any(static registration => !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); + var hasPreciseReflectedRegistrations = registrations.Any(static registration => + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); var hasFullReflectionRegistrations = registrations.Any(static registration => !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName) && - registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); + registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty && + registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); var builder = new StringBuilder(); builder.AppendLine("// "); builder.AppendLine("#nullable enable"); @@ -354,7 +479,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); builder.AppendLine(" if (logger is null)"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); - if (hasReflectedImplementationRegistrations || hasFullReflectionRegistrations) + if (hasReflectedImplementationRegistrations || hasPreciseReflectedRegistrations || hasFullReflectionRegistrations) { builder.AppendLine(); builder.Append(" var registryAssembly = typeof(global::"); @@ -376,6 +501,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator continue; } + if (!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) + { + AppendPreciseReflectedRegistrations(builder, registration, registrationIndex); + continue; + } + if (!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) { AppendReflectionRegistration(builder, registration.ReflectionTypeMetadataName!); @@ -457,6 +588,197 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" }"); } + private static void AppendPreciseReflectedRegistrations( + StringBuilder builder, + ImplementationRegistrationSpec registration, + int registrationIndex) + { + var implementationVariableName = $"implementationType{registrationIndex}"; + if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) + { + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = typeof("); + builder.Append(registration.ImplementationTypeDisplayName); + builder.AppendLine(");"); + } + else + { + var implementationReflectionTypeMetadataName = registration.ReflectionTypeMetadataName!; + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(implementationReflectionTypeMetadataName)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + } + + builder.Append(" if ("); + builder.Append(implementationVariableName); + builder.AppendLine(" is not null)"); + builder.AppendLine(" {"); + + for (var registrationOffset = 0; + registrationOffset < registration.PreciseReflectedRegistrations.Length; + registrationOffset++) + { + var reflectedRegistration = registration.PreciseReflectedRegistrations[registrationOffset]; + var registrationVariablePrefix = $"serviceType{registrationIndex}_{registrationOffset}"; + AppendPreciseReflectedTypeResolution( + builder, + reflectedRegistration.ServiceTypeArguments, + registrationVariablePrefix, + implementationVariableName, + reflectedRegistration.OpenHandlerTypeDisplayName, + registration.ImplementationLogName, + reflectedRegistration.HandlerInterfaceLogName, + 3); + } + + builder.AppendLine(" }"); + } + + private static void AppendPreciseReflectedTypeResolution( + StringBuilder builder, + ImmutableArray serviceTypeArguments, + string registrationVariablePrefix, + string implementationVariableName, + string openHandlerTypeDisplayName, + string implementationLogName, + string handlerInterfaceLogName, + int indentLevel) + { + var indent = new string(' ', indentLevel * 4); + var nestedIndent = new string(' ', (indentLevel + 1) * 4); + var resolvedArgumentNames = new string[serviceTypeArguments.Length]; + var reflectedArgumentNames = new List(); + + for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++) + { + resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + serviceTypeArguments[argumentIndex], + $"{registrationVariablePrefix}Argument{argumentIndex}", + reflectedArgumentNames, + indent); + } + + if (reflectedArgumentNames.Count > 0) + { + builder.Append(indent); + builder.Append("if ("); + for (var index = 0; index < reflectedArgumentNames.Count; index++) + { + if (index > 0) + builder.Append(" && "); + + builder.Append(reflectedArgumentNames[index]); + builder.Append(" is not null"); + } + + builder.AppendLine(")"); + builder.Append(indent); + builder.AppendLine("{"); + indent = nestedIndent; + } + + builder.Append(indent); + builder.Append("var "); + builder.Append(registrationVariablePrefix); + builder.Append(" = typeof("); + builder.Append(openHandlerTypeDisplayName); + builder.Append(").MakeGenericType("); + for (var index = 0; index < resolvedArgumentNames.Length; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append(resolvedArgumentNames[index]); + } + + builder.AppendLine(");"); + builder.Append(indent); + builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.Append(indent); + builder.AppendLine(" services,"); + builder.Append(indent); + builder.Append(" "); + builder.Append(registrationVariablePrefix); + builder.AppendLine(","); + builder.Append(indent); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + builder.Append(indent); + builder.Append("logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(implementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(handlerInterfaceLogName)); + builder.AppendLine(".\");"); + + if (reflectedArgumentNames.Count > 0) + { + builder.Append(new string(' ', indentLevel * 4)); + builder.AppendLine("}"); + } + } + + private static string AppendRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName)) + return $"typeof({runtimeTypeReference.TypeDisplayName})"; + + if (runtimeTypeReference.ArrayElementTypeReference is not null) + { + var elementExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.ArrayElementTypeReference, + $"{variableBaseName}Element", + reflectedArgumentNames, + indent); + + return runtimeTypeReference.ArrayRank == 1 + ? $"{elementExpression}.MakeArrayType()" + : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; + } + + if (runtimeTypeReference.GenericTypeDefinitionReference is not null) + { + var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeDefinitionReference, + $"{variableBaseName}GenericDefinition", + reflectedArgumentNames, + indent); + var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; + for (var argumentIndex = 0; argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; argumentIndex++) + { + genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeArguments[argumentIndex], + $"{variableBaseName}GenericArgument{argumentIndex}", + reflectedArgumentNames, + indent); + } + + return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; + } + + var reflectionTypeMetadataName = runtimeTypeReference.ReflectionTypeMetadataName!; + reflectedArgumentNames.Add(variableBaseName); + builder.Append(indent); + builder.Append("var "); + builder.Append(variableBaseName); + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(reflectionTypeMetadataName)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + return variableBaseName; + } + private static void AppendReflectionHelpers(StringBuilder builder) { // Emit the runtime helper methods only when at least one handler requires metadata-name lookup. @@ -589,11 +911,52 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string HandlerInterfaceDisplayName, string HandlerInterfaceLogName); + private sealed record RuntimeTypeReferenceSpec( + string? TypeDisplayName, + string? ReflectionTypeMetadataName, + RuntimeTypeReferenceSpec? ArrayElementTypeReference, + int ArrayRank, + RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, + ImmutableArray GenericTypeArguments) + { + public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) + { + return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, 0, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) + { + return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, 0, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) + { + return new RuntimeTypeReferenceSpec(null, null, elementTypeReference, arrayRank, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromConstructedGeneric( + RuntimeTypeReferenceSpec genericTypeDefinitionReference, + ImmutableArray genericTypeArguments) + { + return new RuntimeTypeReferenceSpec(null, null, null, 0, genericTypeDefinitionReference, + genericTypeArguments); + } + } + + private readonly record struct PreciseReflectedRegistrationSpec( + string OpenHandlerTypeDisplayName, + string HandlerInterfaceLogName, + ImmutableArray ServiceTypeArguments); + private readonly record struct ImplementationRegistrationSpec( string ImplementationTypeDisplayName, string ImplementationLogName, ImmutableArray DirectRegistrations, ImmutableArray ReflectedImplementationRegistrations, + ImmutableArray PreciseReflectedRegistrations, string? ReflectionTypeMetadataName); private readonly struct HandlerCandidateAnalysis : IEquatable @@ -603,12 +966,14 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string implementationLogName, ImmutableArray registrations, ImmutableArray reflectedImplementationRegistrations, + ImmutableArray preciseReflectedRegistrations, string? reflectionTypeMetadataName) { ImplementationTypeDisplayName = implementationTypeDisplayName; ImplementationLogName = implementationLogName; Registrations = registrations; ReflectedImplementationRegistrations = reflectedImplementationRegistrations; + PreciseReflectedRegistrations = preciseReflectedRegistrations; ReflectionTypeMetadataName = reflectionTypeMetadataName; } @@ -620,6 +985,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public ImmutableArray ReflectedImplementationRegistrations { get; } + public ImmutableArray PreciseReflectedRegistrations { get; } + public string? ReflectionTypeMetadataName { get; } public bool Equals(HandlerCandidateAnalysis other) @@ -630,7 +997,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, StringComparison.Ordinal) || Registrations.Length != other.Registrations.Length || - ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length) + ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || + PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length) { return false; } @@ -648,6 +1016,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } + for (var index = 0; index < PreciseReflectedRegistrations.Length; index++) + { + if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index])) + return false; + } + return true; } @@ -676,6 +1050,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode(); } + foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations) + { + hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode(); + } + return hashCode; } } From 082b2403c70bbb3c8a9c06154a646936d3c3be12 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:56:48 +0800 Subject: [PATCH 62/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8?= =?UTF-8?q?=E6=BA=90=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 CqrsHandlerRegistryGenerator 源代码生成器 - 减少运行时程序集反射扫描成本,提升性能 - 支持 IRequestHandler、INotificationHandler 和 IStreamRequestHandler 接口 - 自动为 CQRS 处理器生成服务注册代码 - 提供精确的运行时类型引用和反射注册功能 - 包含直接引用、数组和泛型类型的支持 - 生成带有日志记录的处理器注册代码 --- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 1119a92c..36a72be5 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -228,6 +228,23 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal); } + /// + /// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。 + /// + /// + /// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。 + /// + /// + /// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。 + /// + /// + /// 当方法返回 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述; + /// 当方法返回 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。 + /// + /// + /// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 ; + /// 只要任一泛型实参无法安全编码到生成输出中,就返回 。 + /// private static bool TryCreatePreciseReflectedRegistration( Compilation compilation, INamedTypeSymbol handlerInterface, @@ -236,7 +253,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition .ConstructUnboundGenericType() .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var typeArguments = ImmutableArray.CreateBuilder(handlerInterface.TypeArguments.Length); + var typeArguments = + ImmutableArray.CreateBuilder(handlerInterface.TypeArguments.Length); foreach (var typeArgument in handlerInterface.TypeArguments) { if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference)) @@ -255,6 +273,23 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return true; } + /// + /// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。 + /// + /// + /// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。 + /// + /// + /// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。 + /// + /// + /// 当方法返回 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示; + /// 当方法返回 时为 ,调用方应回退到更宽泛的实现类型反射扫描策略。 + /// + /// + /// 当 及其递归子结构都能映射为稳定的运行时引用时返回 ; + /// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 。 + /// private static bool TryCreateRuntimeTypeReference( Compilation compilation, ITypeSymbol type, @@ -277,7 +312,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (type is INamedTypeSymbol genericNamedType && genericNamedType.IsGenericType && !genericNamedType.IsUnboundGenericType && - TryCreateGenericTypeDefinitionReference(compilation, genericNamedType, out var genericTypeDefinitionReference)) + TryCreateGenericTypeDefinitionReference(compilation, genericNamedType, + out var genericTypeDefinitionReference)) { var genericTypeArguments = ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); @@ -310,6 +346,23 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } + /// + /// 为已构造泛型类型解析其泛型定义的运行时引用描述。 + /// + /// + /// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。 + /// + /// + /// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。 + /// + /// + /// 当方法返回 时,包含泛型定义的直接引用或反射查找描述; + /// 当方法返回 时为 ,调用方应停止精确构造并回退到更保守的注册路径。 + /// + /// + /// 当泛型定义能够以稳定方式编码到生成输出中时返回 ; + /// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 。 + /// private static bool TryCreateGenericTypeDefinitionReference( Compilation compilation, INamedTypeSymbol genericNamedType, @@ -479,7 +532,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); builder.AppendLine(" if (logger is null)"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); - if (hasReflectedImplementationRegistrations || hasPreciseReflectedRegistrations || hasFullReflectionRegistrations) + if (hasReflectedImplementationRegistrations || hasPreciseReflectedRegistrations || + hasFullReflectionRegistrations) { builder.AppendLine(); builder.Append(" var registryAssembly = typeof(global::"); @@ -697,7 +751,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(");"); builder.Append(indent); - builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine( + "global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); builder.Append(indent); builder.AppendLine(" services,"); builder.Append(indent); @@ -755,7 +810,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator reflectedArgumentNames, indent); var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; - for (var argumentIndex = 0; argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; argumentIndex++) + for (var argumentIndex = 0; + argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; + argumentIndex++) { genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( builder, @@ -765,7 +822,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator indent); } - return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; + return + $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; } var reflectionTypeMetadataName = runtimeTypeReference.ReflectionTypeMetadataName!; From a8386c175940cc686bcfd0428681679c3a208153 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:07:04 +0800 Subject: [PATCH 63/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=88=86=E5=8F=91=E5=99=A8=E5=AE=9E=E7=8E=B0=E5=8F=8A?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 GFramework 自有 CQRS 运行时分发器,支持请求/通知/流式请求处理 - 添加了多层级缓存机制,包括服务类型缓存、调用委托缓存、按响应类型分层缓存 - 实现了上下文感知处理器的自动注入功能 - 集成了管道行为链处理机制,支持中间件模式的请求处理 - 添加了完整的缓存测试用例,验证各种消息类型的缓存命中与复用逻辑 - 优化了反射调用性能,避免热路径中的重复类型构造与装箱操作 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 112 ++++++++++++++++-- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 76 ++++++------ 2 files changed, 147 insertions(+), 41 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 5ae794ad..6d6e6a00 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -7,7 +7,7 @@ using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Cqrs.Tests.Cqrs; /// -/// 验证 CQRS dispatcher 会缓存热路径中的服务类型构造结果。 +/// 验证 CQRS dispatcher 会缓存热路径中的服务类型与调用委托。 /// [TestFixture] internal sealed class CqrsDispatcherCacheTests @@ -29,6 +29,7 @@ internal sealed class CqrsDispatcherCacheTests _container.Freeze(); _context = new ArchitectureContext(_container); + ClearDispatcherCaches(); } /// @@ -45,7 +46,7 @@ internal sealed class CqrsDispatcherCacheTests private ArchitectureContext? _context; /// - /// 验证相同消息类型重复分发时,不会重复扩张服务类型缓存。 + /// 验证相同消息类型重复分发时,不会重复扩张服务类型与调用委托缓存。 /// [Test] public async Task Dispatcher_Should_Cache_Service_Types_After_First_Dispatch() @@ -53,8 +54,8 @@ internal sealed class CqrsDispatcherCacheTests var notificationServiceTypes = GetCacheField("NotificationHandlerServiceTypes"); var requestServiceTypes = GetCacheField("RequestServiceTypes"); var streamServiceTypes = GetCacheField("StreamHandlerServiceTypes"); - var requestInvokers = GetCacheField("RequestInvokers"); - var requestPipelineInvokers = GetCacheField("RequestPipelineInvokers"); + var requestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers"); + var requestPipelineInvokers = GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers"); var notificationInvokers = GetCacheField("NotificationInvokers"); var streamInvokers = GetCacheField("StreamInvokers"); @@ -104,14 +105,42 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证 request 调用委托会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。 + /// + [Test] + public async Task Dispatcher_Should_Cache_Request_Invokers_Per_Response_Type() + { + var intRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers"); + var stringRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers"); + + var intBefore = intRequestInvokers.Count; + var stringBefore = stringRequestInvokers.Count; + + await _context!.SendRequestAsync(new DispatcherCacheRequest()); + await _context.SendRequestAsync(new DispatcherStringCacheRequest()); + + var intAfterFirstDispatch = intRequestInvokers.Count; + var stringAfterFirstDispatch = stringRequestInvokers.Count; + + await _context.SendRequestAsync(new DispatcherCacheRequest()); + await _context.SendRequestAsync(new DispatcherStringCacheRequest()); + + Assert.Multiple(() => + { + Assert.That(intAfterFirstDispatch, Is.EqualTo(intBefore + 1)); + Assert.That(stringAfterFirstDispatch, Is.EqualTo(stringBefore + 1)); + Assert.That(intRequestInvokers.Count, Is.EqualTo(intAfterFirstDispatch)); + Assert.That(stringRequestInvokers.Count, Is.EqualTo(stringAfterFirstDispatch)); + }); + } + /// /// 通过反射读取 dispatcher 的静态缓存字典。 /// private static IDictionary GetCacheField(string fieldName) { - var dispatcherType = typeof(CqrsReflectionFallbackAttribute).Assembly - .GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!; - + var dispatcherType = GetDispatcherType(); var field = dispatcherType.GetField( fieldName, BindingFlags.NonPublic | BindingFlags.Static); @@ -123,6 +152,56 @@ internal sealed class CqrsDispatcherCacheTests $"Dispatcher cache field {fieldName} does not implement IDictionary."); } + /// + /// 清空本测试依赖的 dispatcher 静态缓存,避免跨用例共享进程级状态导致断言漂移。 + /// + private static void ClearDispatcherCaches() + { + GetCacheField("NotificationHandlerServiceTypes").Clear(); + GetCacheField("RequestServiceTypes").Clear(); + GetCacheField("StreamHandlerServiceTypes").Clear(); + GetCacheField("NotificationInvokers").Clear(); + GetCacheField("StreamInvokers").Clear(); + GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers").Clear(); + GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers").Clear(); + GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers").Clear(); + } + + /// + /// 通过反射读取 dispatcher 嵌套泛型缓存类型上的静态缓存字典。 + /// + private static IDictionary GetGenericCacheField(string nestedTypeName, Type genericTypeArgument, string fieldName) + { + var nestedGenericType = GetDispatcherType().GetNestedType( + nestedTypeName, + BindingFlags.NonPublic); + + Assert.That(nestedGenericType, Is.Not.Null, $"Missing dispatcher nested cache type {nestedTypeName}."); + + var closedNestedType = nestedGenericType!.MakeGenericType(genericTypeArgument); + var field = closedNestedType.GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That( + field, + Is.Not.Null, + $"Missing dispatcher nested cache field {nestedTypeName}.{fieldName} for {genericTypeArgument.FullName}."); + + return field!.GetValue(null) as IDictionary + ?? throw new InvalidOperationException( + $"Dispatcher nested cache field {nestedTypeName}.{fieldName} does not implement IDictionary."); + } + + /// + /// 获取 CQRS dispatcher 运行时类型。 + /// + private static Type GetDispatcherType() + { + return typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!; + } + /// /// 消费整个异步流,确保建流路径被真实执行。 /// @@ -154,6 +233,11 @@ internal sealed record DispatcherCacheStreamRequest : IStreamRequest; /// internal sealed record DispatcherPipelineCacheRequest : IRequest; +/// +/// 用于验证按响应类型分层 request invoker 缓存的测试请求。 +/// +internal sealed record DispatcherStringCacheRequest : IRequest; + /// /// 处理 。 /// @@ -213,6 +297,20 @@ internal sealed class DispatcherPipelineCacheRequestHandler : IRequestHandler +/// 处理 。 +/// +internal sealed class DispatcherStringCacheRequestHandler : IRequestHandler +{ + /// + /// 返回固定字符串,供按响应类型缓存测试验证 string 路径。 + /// + public ValueTask Handle(DispatcherStringCacheRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult("dispatcher-cache"); + } +} + /// /// 为 提供最小 pipeline 行为, /// 用于命中 dispatcher 的 pipeline invoker 缓存分支。 diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 002b7edc..a6d62f96 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -15,16 +15,6 @@ internal sealed class CqrsDispatcher( IIocContainer container, ILogger logger) : ICqrsRuntime { - // 进程级缓存:按请求/响应类型缓存直接处理器调用委托,避免热路径重复反射。 - // 线程安全依赖 ConcurrentDictionary;缓存与进程同寿命,默认假设请求类型集合有限且稳定。 - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> - RequestInvokers = new(); - - // 进程级缓存:缓存带 pipeline 的请求调用委托,减少每次分发时的反射与表达式重建开销。 - // 若后续引入动态生成请求类型,需要重新评估该缓存的增长边界。 - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> - RequestPipelineInvokers = new(); - // 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。 private static readonly ConcurrentDictionary NotificationInvokers = new(); @@ -131,20 +121,18 @@ internal sealed class CqrsDispatcher( if (behaviors.Count == 0) { - var invoker = RequestInvokers.GetOrAdd( - (requestType, typeof(TResponse)), - static key => CreateRequestInvoker(key.RequestType, key.ResponseType)); + var invoker = RequestInvokerCache.Invokers.GetOrAdd( + requestType, + CreateRequestInvoker); - var result = await invoker(handler, request, cancellationToken); - return result is null ? default! : (TResponse)result; + return await invoker(handler, request, cancellationToken); } - var pipelineInvoker = RequestPipelineInvokers.GetOrAdd( - (requestType, typeof(TResponse)), - static key => CreateRequestPipelineInvoker(key.RequestType, key.ResponseType)); + var pipelineInvoker = RequestPipelineInvokerCache.Invokers.GetOrAdd( + requestType, + CreateRequestPipelineInvoker); - var pipelineResult = await pipelineInvoker(handler, behaviors, request, cancellationToken); - return pipelineResult is null ? default! : (TResponse)pipelineResult; + return await pipelineInvoker(handler, behaviors, request, cancellationToken); } /// @@ -200,21 +188,23 @@ internal sealed class CqrsDispatcher( /// /// 生成请求处理器调用委托,避免每次发送都重复反射。 /// - private static RequestInvoker CreateRequestInvoker(Type requestType, Type responseType) + private static RequestInvoker CreateRequestInvoker(Type requestType) { var method = RequestHandlerInvokerMethodDefinition - .MakeGenericMethod(requestType, responseType); - return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); + .MakeGenericMethod(requestType, typeof(TResponse)); + return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); } /// /// 生成带管道行为的请求处理委托,避免每次发送都重复反射。 /// - private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType, Type responseType) + private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType) { var method = RequestPipelineInvokerMethodDefinition - .MakeGenericMethod(requestType, responseType); - return (RequestPipelineInvoker)Delegate.CreateDelegate(typeof(RequestPipelineInvoker), method); + .MakeGenericMethod(requestType, typeof(TResponse)); + return (RequestPipelineInvoker)Delegate.CreateDelegate( + typeof(RequestPipelineInvoker), + method); } /// @@ -240,7 +230,7 @@ internal sealed class CqrsDispatcher( /// /// 执行已强类型化的请求处理器调用。 /// - private static async ValueTask InvokeRequestHandlerAsync( + private static ValueTask InvokeRequestHandlerAsync( object handler, object request, CancellationToken cancellationToken) @@ -248,14 +238,13 @@ internal sealed class CqrsDispatcher( { var typedHandler = (IRequestHandler)handler; var typedRequest = (TRequest)request; - var result = await typedHandler.Handle(typedRequest, cancellationToken); - return result; + return typedHandler.Handle(typedRequest, cancellationToken); } /// /// 执行包含管道行为链的请求处理。 /// - private static async ValueTask InvokeRequestPipelineAsync( + private static ValueTask InvokeRequestPipelineAsync( object handler, IReadOnlyList behaviors, object request, @@ -275,8 +264,7 @@ internal sealed class CqrsDispatcher( next = (message, token) => behavior.Handle(message, currentNext, token); } - var result = await next(typedRequest, cancellationToken); - return result; + return next(typedRequest, cancellationToken); } /// @@ -307,10 +295,12 @@ internal sealed class CqrsDispatcher( return typedHandler.Handle(typedRequest, cancellationToken); } - private delegate ValueTask RequestInvoker(object handler, object request, + private delegate ValueTask RequestInvoker( + object handler, + object request, CancellationToken cancellationToken); - private delegate ValueTask RequestPipelineInvoker( + private delegate ValueTask RequestPipelineInvoker( object handler, IReadOnlyList behaviors, object request, @@ -321,5 +311,23 @@ internal sealed class CqrsDispatcher( private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + /// + /// 按响应类型分层缓存 request 处理器调用委托,避免 value-type 响应在 object 桥接中产生装箱。 + /// + /// 请求响应类型。 + private static class RequestInvokerCache + { + internal static readonly ConcurrentDictionary> Invokers = new(); + } + + /// + /// 按响应类型分层缓存带 pipeline 的 request 调用委托,避免 pipeline 热路径上的额外装箱。 + /// + /// 请求响应类型。 + private static class RequestPipelineInvokerCache + { + internal static readonly ConcurrentDictionary> Invokers = new(); + } + private readonly record struct RequestServiceTypeSet(Type HandlerType, Type BehaviorType); } From 1792fafc85269b929d121687bee4416bc319250b Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:24:52 +0800 Subject: [PATCH 64/70] =?UTF-8?q?refactor(Cqrs):=20=E9=87=8D=E6=9E=84CQRS?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=BB=A5=E6=94=AF=E6=8C=81=E6=B7=B7=E5=90=88?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改注册条件判断逻辑,支持多种注册类型的组合处理 - 新增有序注册实现方法,统一处理直接、反射和精确反射注册 - 添加注册类型枚举以区分不同的注册方式 - 实现混合注册场景下的稳定排序机制 - 更新反射注册逻辑以支持更复杂的类型解析 - 优化代码结构提升可读性和维护性 - 添加单元测试验证各种混合注册场景的正确性 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 246 ++++++++++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 198 ++++++++------ 2 files changed, 371 insertions(+), 73 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index ef70ec4a..3026d279 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -164,6 +164,94 @@ public class CqrsHandlerRegistryGeneratorTests """; + private const string MixedDirectAndPreciseRegistrationsExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = typeof(global::TestApp.Container.MixedHandler); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1Element = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1Element is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType()); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.MixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.MixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + + """; + + private const string MixedReflectedImplementationAndPreciseRegistrationsExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenMixedHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1Element = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1Element is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType()); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenMixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenMixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -579,6 +667,164 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected)); } + /// + /// 验证同一个 implementation 同时包含可直接注册接口与需精确重建接口时, + /// 生成器会保留两类注册,并继续按 handler interface 名称稳定排序。 + /// + [Test] + public async Task Generates_Mixed_Direct_And_Precise_Registrations_For_Same_Implementation() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [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() : IRequest; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + public sealed class MixedHandler : + IRequestHandler, + IRequestHandler + { + } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", MixedDirectAndPreciseRegistrationsExpected)); + } + + /// + /// 验证隐藏 implementation 同时包含可见 handler interface 与需精确重建接口时, + /// 生成器会保留两类注册,而不会让可见接口被整实现回退吞掉。 + /// + [Test] + public async Task Generates_Mixed_Reflected_Implementation_And_Precise_Registrations_For_Same_Implementation() + { + const string source = """ + using System; + + 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 { } + 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); + } + + [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() : IRequest; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenMixedHandler : + IRequestHandler, + IRequestHandler + { + } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", MixedReflectedImplementationAndPreciseRegistrationsExpected)); + } + /// /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 36a72be5..f3bb5e96 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -549,40 +549,22 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++) { var registration = registrations[registrationIndex]; - if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty) + if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty || + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) { - AppendReflectedImplementationRegistrations(builder, registration, registrationIndex); - continue; + AppendOrderedImplementationRegistrations(builder, registration, registrationIndex); + } + else if (!registration.DirectRegistrations.IsDefaultOrEmpty) + { + AppendDirectRegistrations(builder, registration); } - if (!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) - { - AppendPreciseReflectedRegistrations(builder, registration, registrationIndex); - continue; - } - - if (!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) + if (!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName) && + registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty && + registration.PreciseReflectedRegistrations.IsDefaultOrEmpty && + registration.DirectRegistrations.IsDefaultOrEmpty) { AppendReflectionRegistration(builder, registration.ReflectionTypeMetadataName!); - continue; - } - - foreach (var directRegistration in registration.DirectRegistrations) - { - builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.Append(" typeof("); - builder.Append(directRegistration.HandlerInterfaceDisplayName); - builder.AppendLine("),"); - builder.Append(" typeof("); - builder.Append(directRegistration.ImplementationTypeDisplayName); - builder.AppendLine("));"); - builder.Append(" logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(directRegistration.ImplementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); - builder.AppendLine(".\");"); } } @@ -605,48 +587,71 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine("\");"); } - private static void AppendReflectedImplementationRegistrations( + private static void AppendDirectRegistrations( StringBuilder builder, - ImplementationRegistrationSpec registration, - int registrationIndex) + ImplementationRegistrationSpec registration) { - var implementationVariableName = $"implementationType{registrationIndex}"; - builder.Append(" var "); - builder.Append(implementationVariableName); - builder.Append(" = registryAssembly.GetType(\""); - builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); - builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); - builder.Append(" if ("); - builder.Append(implementationVariableName); - builder.AppendLine(" is not null)"); - builder.AppendLine(" {"); - - foreach (var reflectedRegistration in registration.ReflectedImplementationRegistrations) + foreach (var directRegistration in registration.DirectRegistrations) { builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.Append(" typeof("); - builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); builder.AppendLine("),"); - builder.Append(" "); - builder.Append(implementationVariableName); - builder.AppendLine(");"); - builder.Append(" logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" typeof("); + builder.Append(directRegistration.ImplementationTypeDisplayName); + builder.AppendLine("));"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(directRegistration.ImplementationLogName)); builder.Append(" as "); - builder.Append(EscapeStringLiteral(reflectedRegistration.HandlerInterfaceLogName)); + builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); builder.AppendLine(".\");"); } - - builder.AppendLine(" }"); } - private static void AppendPreciseReflectedRegistrations( + private static void AppendOrderedImplementationRegistrations( StringBuilder builder, ImplementationRegistrationSpec registration, int registrationIndex) { + var orderedRegistrations = + new List<(string HandlerInterfaceLogName, OrderedRegistrationKind Kind, int Index)>( + registration.DirectRegistrations.Length + + registration.ReflectedImplementationRegistrations.Length + + registration.PreciseReflectedRegistrations.Length); + + for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++) + { + orderedRegistrations.Add(( + registration.DirectRegistrations[directIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.Direct, + directIndex)); + } + + for (var reflectedIndex = 0; + reflectedIndex < registration.ReflectedImplementationRegistrations.Length; + reflectedIndex++) + { + orderedRegistrations.Add(( + registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.ReflectedImplementation, + reflectedIndex)); + } + + for (var preciseIndex = 0; + preciseIndex < registration.PreciseReflectedRegistrations.Length; + preciseIndex++) + { + orderedRegistrations.Add(( + registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.PreciseReflected, + preciseIndex)); + } + + orderedRegistrations.Sort(static (left, right) => + StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); + var implementationVariableName = $"implementationType{registrationIndex}"; if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) { @@ -658,11 +663,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } else { - var implementationReflectionTypeMetadataName = registration.ReflectionTypeMetadataName!; builder.Append(" var "); builder.Append(implementationVariableName); builder.Append(" = registryAssembly.GetType(\""); - builder.Append(EscapeStringLiteral(implementationReflectionTypeMetadataName)); + builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); } @@ -671,21 +675,62 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" is not null)"); builder.AppendLine(" {"); - for (var registrationOffset = 0; - registrationOffset < registration.PreciseReflectedRegistrations.Length; - registrationOffset++) + foreach (var orderedRegistration in orderedRegistrations) { - var reflectedRegistration = registration.PreciseReflectedRegistrations[registrationOffset]; - var registrationVariablePrefix = $"serviceType{registrationIndex}_{registrationOffset}"; - AppendPreciseReflectedTypeResolution( - builder, - reflectedRegistration.ServiceTypeArguments, - registrationVariablePrefix, - implementationVariableName, - reflectedRegistration.OpenHandlerTypeDisplayName, - registration.ImplementationLogName, - reflectedRegistration.HandlerInterfaceLogName, - 3); + switch (orderedRegistration.Kind) + { + case OrderedRegistrationKind.Direct: + var directRegistration = registration.DirectRegistrations[orderedRegistration.Index]; + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + break; + case OrderedRegistrationKind.ReflectedImplementation: + var reflectedRegistration = + registration.ReflectedImplementationRegistrations[orderedRegistration.Index]; + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(reflectedRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + break; + case OrderedRegistrationKind.PreciseReflected: + var preciseRegistration = registration.PreciseReflectedRegistrations[orderedRegistration.Index]; + var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistration.Index}"; + AppendPreciseReflectedTypeResolution( + builder, + preciseRegistration.ServiceTypeArguments, + registrationVariablePrefix, + implementationVariableName, + preciseRegistration.OpenHandlerTypeDisplayName, + registration.ImplementationLogName, + preciseRegistration.HandlerInterfaceLogName, + 3); + break; + default: + throw new InvalidOperationException( + $"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}."); + } } builder.AppendLine(" }"); @@ -969,6 +1014,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string HandlerInterfaceDisplayName, string HandlerInterfaceLogName); + private enum OrderedRegistrationKind + { + Direct, + ReflectedImplementation, + PreciseReflected + } + private sealed record RuntimeTypeReferenceSpec( string? TypeDisplayName, string? ReflectionTypeMetadataName, From eca2d67529a76dde0e9e30f99dc362b502059a1e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:41:20 +0800 Subject: [PATCH 65/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了CqrsHandlerRegistryGenerator源代码生成器 - 为CQRS处理器减少运行时程序集反射扫描开销 - 支持IRequestHandler、INotificationHandler和IStreamRequestHandler接口 - 提供静态类型引用和运行时反射发现的混合注册策略 - 生成服务注册代码并添加调试日志记录功能 - 实现精确的运行时类型引用描述和泛型类型处理 --- .../Core/GeneratorTest.cs | 23 ++- .../Core/MetadataReferenceTestBuilder.cs | 65 +++++++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 164 +++++++++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 165 ++++++++++-------- 4 files changed, 347 insertions(+), 70 deletions(-) create mode 100644 GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs index a622d387..bed493e7 100644 --- a/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs @@ -16,6 +16,24 @@ public static class GeneratorTest public static async Task RunAsync( string source, params (string filename, string content)[] generatedSources) + { + await RunAsync( + source, + additionalReferences: [], + generatedSources); + } + + /// + /// 运行源代码生成器测试,并为测试编译显式追加元数据引用。 + /// + /// 输入的源代码。 + /// 附加元数据引用,用于构造多程序集场景。 + /// 期望生成的源文件集合,包含文件名和内容的元组。 + /// 异步操作任务。 + public static async Task RunAsync( + string source, + IEnumerable additionalReferences, + params (string filename, string content)[] generatedSources) { var test = new CSharpSourceGeneratorTest { @@ -31,6 +49,9 @@ public static class GeneratorTest test.TestState.GeneratedSources.Add( (typeof(TGenerator), filename, NormalizeLineEndings(content))); + foreach (var additionalReference in additionalReferences) + test.TestState.AdditionalReferences.Add(additionalReference); + await test.RunAsync(); } @@ -46,4 +67,4 @@ public static class GeneratorTest .Replace("\r", "\n", StringComparison.Ordinal) .Replace("\n", Environment.NewLine, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs b/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs new file mode 100644 index 00000000..173a88d9 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using System.IO; +using Microsoft.CodeAnalysis.CSharp; + +namespace GFramework.SourceGenerators.Tests.Core; + +/// +/// 为多程序集源生成器测试构建内存元数据引用。 +/// +public static class MetadataReferenceTestBuilder +{ + /// + /// 将给定源码编译为内存程序集,并返回可供测试编译消费的元数据引用。 + /// + /// 目标程序集名称。 + /// 待编译源码。 + /// 附加元数据引用,用于构造依赖链。 + /// 编译成功后的内存元数据引用。 + public static MetadataReference CreateFromSource( + string assemblyName, + string source, + params MetadataReference[] additionalReferences) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = GetRuntimeMetadataReferences() + .Concat(additionalReferences) + .ToImmutableArray(); + var compilation = CSharpCompilation.Create( + assemblyName, + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using var stream = new MemoryStream(); + var emitResult = compilation.Emit(stream); + if (!emitResult.Success) + { + var diagnostics = string.Join( + Environment.NewLine, + emitResult.Diagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .Select(static diagnostic => diagnostic.ToString())); + throw new InvalidOperationException( + $"Failed to build metadata reference '{assemblyName}'.{Environment.NewLine}{diagnostics}"); + } + + stream.Position = 0; + return MetadataReference.CreateFromImage(stream.ToArray()); + } + + /// + /// 获取当前测试运行时可直接复用的基础元数据引用集合。 + /// + /// 当前运行时可信平台程序集对应的元数据引用。 + public static ImmutableArray GetRuntimeMetadataReferences() + { + var trustedPlatformAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))? + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + ?? Array.Empty(); + + return trustedPlatformAssemblies + .Select(static path => (MetadataReference)MetadataReference.CreateFromFile(path)) + .ToImmutableArray(); + } +} diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 3026d279..dc8c9abb 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1,6 +1,7 @@ using System.Reflection; using GFramework.SourceGenerators.Cqrs; using GFramework.SourceGenerators.Tests.Core; +using Microsoft.CodeAnalysis.CSharp; namespace GFramework.SourceGenerators.Tests.Cqrs; @@ -825,6 +826,135 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", MixedReflectedImplementationAndPreciseRegistrationsExpected)); } + /// + /// 验证当外部基类暴露的 handler interface 含有生成注册器顶层上下文不可直接引用的 protected 类型时, + /// 生成器会保留已知直注册,并只对剩余未知接口做本地 interface discovery。 + /// + [Test] + public void Generates_Partial_Runtime_Interface_Discovery_For_Inaccessible_External_Protected_Types() + { + const string contractsSource = """ + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + """; + + const string dependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public sealed record VisibleRequest() : IRequest; + + public abstract class VisibilityScope + { + protected internal sealed record ProtectedResponse(); + + protected internal sealed record ProtectedRequest() : IRequest; + } + + public abstract class HandlerBase : + VisibilityScope, + IRequestHandler, + IRequestHandler + { + } + """; + + const string source = """ + using System; + using Dep; + + 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 + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + public sealed class DerivedHandler : HandlerBase + { + } + } + """; + + var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( + "Contracts", + contractsSource); + var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( + "Dependency", + dependencySource, + contractsReference); + var generatedSource = RunGenerator( + source, + contractsReference, + dependencyReference); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain("var implementationType0 = typeof(global::TestApp.DerivedHandler);")); + Assert.That( + generatedSource, + Does.Contain( + "var knownServiceTypes0 = new global::System.Collections.Generic.HashSet();")); + Assert.That( + generatedSource, + Does.Contain( + "knownServiceTypes0.Add(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler));")); + Assert.That( + generatedSource, + Does.Contain( + "RegisterRemainingReflectedHandlerInterfaces(services, logger, implementationType0, knownServiceTypes0);")); + Assert.That( + generatedSource, + Does.Contain("if (knownServiceTypes.Contains(handlerInterface))")); + Assert.That( + generatedSource, + Does.Contain( + "Registered CQRS handler TestApp.DerivedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler.")); + Assert.That( + generatedSource, + Does.Not.Contain( + "typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, /// 不再输出 fallback marker。 @@ -999,4 +1129,38 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That(escaped, Is.EqualTo(expected)); } + + /// + /// 运行 CQRS handler registry generator,并返回单个生成文件的源码文本。 + /// + private static string RunGenerator( + string source, + params MetadataReference[] additionalReferences) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create( + "TestProject", + [syntaxTree], + MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()], + parseOptions: (CSharpParseOptions)syntaxTree.Options); + driver = driver.RunGeneratorsAndUpdateCompilation( + compilation, + out var updatedCompilation, + out _); + + var compilationErrors = updatedCompilation.GetDiagnostics() + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.That(compilationErrors, Is.Empty, string.Join(Environment.NewLine, compilationErrors)); + + var runResult = driver.GetRunResult(); + Assert.That(runResult.Results, Has.Length.EqualTo(1)); + Assert.That(runResult.Results[0].GeneratedSources, Has.Length.EqualTo(1)); + + return runResult.Results[0].GeneratedSources[0].SourceText.ToString(); + } } diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index f3bb5e96..49065f0c 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -86,15 +86,17 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var implementationLogName = GetLogDisplayName(type); - var canReferenceImplementation = CanReferenceFromGeneratedRegistry(type); + var canReferenceImplementation = CanReferenceFromGeneratedRegistry(context.SemanticModel.Compilation, type); var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); var reflectedImplementationRegistrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); var preciseReflectedRegistrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var requiresRuntimeInterfaceDiscovery = false; foreach (var handlerInterface in handlerInterfaces) { - var canReferenceHandlerInterface = CanReferenceFromGeneratedRegistry(handlerInterface); + var canReferenceHandlerInterface = + CanReferenceFromGeneratedRegistry(context.SemanticModel.Compilation, handlerInterface); if (canReferenceImplementation && canReferenceHandlerInterface) { registrations.Add(new HandlerRegistrationSpec( @@ -122,16 +124,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator continue; } - // Some closed handler interfaces still contain runtime-only type shapes such as arrays closed over - // non-public element types. For those rare cases keep the narrow implementation lookup, but let the - // generated registry discover the exact supported interfaces from the implementation type at runtime. - return new HandlerCandidateAnalysis( - implementationTypeDisplayName, - implementationLogName, - ImmutableArray.Empty, - ImmutableArray.Empty, - ImmutableArray.Empty, - GetReflectionTypeMetadataName(type)); + // 某些关闭 handler interface 仍包含只能在实现类型运行时语义里解析的类型形态。 + // 对这些边角场景保留“已知接口静态注册 + 剩余接口运行时补洞”的组合路径, + // 避免单个未知接口把同实现上的其它已知注册全部拖回整实现反射发现。 + requiresRuntimeInterfaceDiscovery = true; } return new HandlerCandidateAnalysis( @@ -140,7 +136,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator registrations.ToImmutable(), reflectedImplementationRegistrations.ToImmutable(), preciseReflectedRegistrations.ToImmutable(), - canReferenceImplementation ? null : GetReflectionTypeMetadataName(type)); + canReferenceImplementation ? null : GetReflectionTypeMetadataName(type), + requiresRuntimeInterfaceDiscovery); } private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, @@ -184,7 +181,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator candidate.Registrations, candidate.ReflectedImplementationRegistrations, candidate.PreciseReflectedRegistrations, - candidate.ReflectionTypeMetadataName)); + candidate.ReflectionTypeMetadataName, + candidate.RequiresRuntimeInterfaceDiscovery)); } registrations.Sort(static (left, right) => @@ -295,7 +293,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ITypeSymbol type, out RuntimeTypeReferenceSpec? runtimeTypeReference) { - if (CanReferenceFromGeneratedRegistry(type)) + if (CanReferenceFromGeneratedRegistry(compilation, type)) { runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); @@ -369,7 +367,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator out RuntimeTypeReferenceSpec? genericTypeDefinitionReference) { var genericTypeDefinition = genericNamedType.OriginalDefinition; - if (CanReferenceFromGeneratedRegistry(genericTypeDefinition)) + if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition)) { genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference( genericTypeDefinition @@ -389,19 +387,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } - private static bool CanReferenceFromGeneratedRegistry(ITypeSymbol type) + private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type) { switch (type) { case IArrayTypeSymbol arrayType: - return CanReferenceFromGeneratedRegistry(arrayType.ElementType); + return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType); case INamedTypeSymbol namedType: - if (!IsTypeChainAccessible(namedType)) + if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null)) return false; - return namedType.TypeArguments.All(CanReferenceFromGeneratedRegistry); + foreach (var typeArgument in namedType.TypeArguments) + { + if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument)) + return false; + } + + return true; case IPointerTypeSymbol pointerType: - return CanReferenceFromGeneratedRegistry(pointerType.PointedAtType); + return CanReferenceFromGeneratedRegistry(compilation, pointerType.PointedAtType); case ITypeParameterSymbol: return false; default: @@ -409,23 +413,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } } - private static bool IsTypeChainAccessible(INamedTypeSymbol type) - { - for (var current = type; current is not null; current = current.ContainingType) - { - if (!IsSymbolAccessible(current)) - return false; - } - - return true; - } - - private static bool IsSymbolAccessible(ISymbol symbol) - { - return symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal - or Accessibility.ProtectedOrInternal; - } - private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type) { var nestedTypes = new Stack(); @@ -496,10 +483,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); var hasPreciseReflectedRegistrations = registrations.Any(static registration => !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); - var hasFullReflectionRegistrations = registrations.Any(static registration => - !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName) && - registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty && - registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); + var hasRuntimeInterfaceDiscovery = registrations.Any(static registration => + registration.RequiresRuntimeInterfaceDiscovery); var builder = new StringBuilder(); builder.AppendLine("// "); builder.AppendLine("#nullable enable"); @@ -533,7 +518,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" if (logger is null)"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); if (hasReflectedImplementationRegistrations || hasPreciseReflectedRegistrations || - hasFullReflectionRegistrations) + registrations.Any(static registration => + !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName))) { builder.AppendLine(); builder.Append(" var registryAssembly = typeof(global::"); @@ -550,7 +536,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { var registration = registrations[registrationIndex]; if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty || - !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty || + registration.RequiresRuntimeInterfaceDiscovery) { AppendOrderedImplementationRegistrations(builder, registration, registrationIndex); } @@ -558,19 +545,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { AppendDirectRegistrations(builder, registration); } - - if (!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName) && - registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty && - registration.PreciseReflectedRegistrations.IsDefaultOrEmpty && - registration.DirectRegistrations.IsDefaultOrEmpty) - { - AppendReflectionRegistration(builder, registration.ReflectionTypeMetadataName!); - } } builder.AppendLine(" }"); - if (hasFullReflectionRegistrations) + if (hasRuntimeInterfaceDiscovery) { builder.AppendLine(); AppendReflectionHelpers(builder); @@ -580,13 +559,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return builder.ToString(); } - private static void AppendReflectionRegistration(StringBuilder builder, string reflectionTypeMetadataName) - { - builder.Append(" RegisterReflectedHandler(services, logger, registryAssembly, \""); - builder.Append(EscapeStringLiteral(reflectionTypeMetadataName)); - builder.AppendLine("\");"); - } - private static void AppendDirectRegistrations( StringBuilder builder, ImplementationRegistrationSpec registration) @@ -653,6 +625,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); var implementationVariableName = $"implementationType{registrationIndex}"; + var knownServiceTypesVariableName = $"knownServiceTypes{registrationIndex}"; if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) { builder.Append(" var "); @@ -675,12 +648,28 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" is not null)"); builder.AppendLine(" {"); + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" var "); + builder.Append(knownServiceTypesVariableName); + builder.AppendLine(" = new global::System.Collections.Generic.HashSet();"); + } + foreach (var orderedRegistration in orderedRegistrations) { switch (orderedRegistration.Kind) { case OrderedRegistrationKind.Direct: var directRegistration = registration.DirectRegistrations[orderedRegistration.Index]; + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" "); + builder.Append(knownServiceTypesVariableName); + builder.Append(".Add(typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("));"); + } + builder.AppendLine( " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); builder.AppendLine(" services,"); @@ -699,6 +688,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator case OrderedRegistrationKind.ReflectedImplementation: var reflectedRegistration = registration.ReflectedImplementationRegistrations[orderedRegistration.Index]; + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" "); + builder.Append(knownServiceTypesVariableName); + builder.Append(".Add(typeof("); + builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("));"); + } + builder.AppendLine( " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); builder.AppendLine(" services,"); @@ -725,6 +723,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator preciseRegistration.OpenHandlerTypeDisplayName, registration.ImplementationLogName, preciseRegistration.HandlerInterfaceLogName, + knownServiceTypesVariableName, + registration.RequiresRuntimeInterfaceDiscovery, 3); break; default: @@ -733,6 +733,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } } + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" RegisterRemainingReflectedHandlerInterfaces(services, logger, "); + builder.Append(implementationVariableName); + builder.Append(", "); + builder.Append(knownServiceTypesVariableName); + builder.AppendLine(");"); + } + builder.AppendLine(" }"); } @@ -744,6 +753,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string openHandlerTypeDisplayName, string implementationLogName, string handlerInterfaceLogName, + string knownServiceTypesVariableName, + bool trackKnownServiceTypes, int indentLevel) { var indent = new string(' ', indentLevel * 4); @@ -808,6 +819,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append(" "); builder.Append(implementationVariableName); builder.AppendLine(");"); + if (trackKnownServiceTypes) + { + builder.Append(indent); + builder.Append(knownServiceTypesVariableName); + builder.Append(".Add("); + builder.Append(registrationVariablePrefix); + builder.AppendLine(");"); + } + builder.Append(indent); builder.Append("logger.Debug(\"Registered CQRS handler "); builder.Append(EscapeStringLiteral(implementationLogName)); @@ -884,15 +904,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private static void AppendReflectionHelpers(StringBuilder builder) { - // Emit the runtime helper methods only when at least one handler requires metadata-name lookup. + // Emit the runtime helper methods only when at least one handler still needs implementation-scoped + // interface discovery after all direct / precise registrations have been emitted. builder.AppendLine( - " private static void RegisterReflectedHandler(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Reflection.Assembly registryAssembly, string implementationTypeMetadataName)"); + " private static void RegisterRemainingReflectedHandlerInterfaces(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Type implementationType, global::System.Collections.Generic.ISet knownServiceTypes)"); builder.AppendLine(" {"); - builder.AppendLine( - " var implementationType = registryAssembly.GetType(implementationTypeMetadataName, throwOnError: false, ignoreCase: false);"); - builder.AppendLine(" if (implementationType is null)"); - builder.AppendLine(" return;"); - builder.AppendLine(); builder.AppendLine(" var handlerInterfaces = implementationType.GetInterfaces();"); builder.AppendLine(" global::System.Array.Sort(handlerInterfaces, CompareTypes);"); builder.AppendLine(); @@ -901,6 +917,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" if (!IsSupportedHandlerInterface(handlerInterface))"); builder.AppendLine(" continue;"); builder.AppendLine(); + builder.AppendLine(" if (knownServiceTypes.Contains(handlerInterface))"); + builder.AppendLine(" continue;"); + builder.AppendLine(); builder.AppendLine( " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); builder.AppendLine(" services,"); @@ -908,6 +927,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" implementationType);"); builder.AppendLine( " logger.Debug($\"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}.\");"); + builder.AppendLine(" knownServiceTypes.Add(handlerInterface);"); builder.AppendLine(" }"); builder.AppendLine(" }"); builder.AppendLine(); @@ -1067,7 +1087,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray DirectRegistrations, ImmutableArray ReflectedImplementationRegistrations, ImmutableArray PreciseReflectedRegistrations, - string? ReflectionTypeMetadataName); + string? ReflectionTypeMetadataName, + bool RequiresRuntimeInterfaceDiscovery); private readonly struct HandlerCandidateAnalysis : IEquatable { @@ -1077,7 +1098,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray registrations, ImmutableArray reflectedImplementationRegistrations, ImmutableArray preciseReflectedRegistrations, - string? reflectionTypeMetadataName) + string? reflectionTypeMetadataName, + bool requiresRuntimeInterfaceDiscovery) { ImplementationTypeDisplayName = implementationTypeDisplayName; ImplementationLogName = implementationLogName; @@ -1085,6 +1107,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ReflectedImplementationRegistrations = reflectedImplementationRegistrations; PreciseReflectedRegistrations = preciseReflectedRegistrations; ReflectionTypeMetadataName = reflectionTypeMetadataName; + RequiresRuntimeInterfaceDiscovery = requiresRuntimeInterfaceDiscovery; } public string ImplementationTypeDisplayName { get; } @@ -1099,6 +1122,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public string? ReflectionTypeMetadataName { get; } + public bool RequiresRuntimeInterfaceDiscovery { get; } + public bool Equals(HandlerCandidateAnalysis other) { if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, @@ -1106,6 +1131,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, StringComparison.Ordinal) || + RequiresRuntimeInterfaceDiscovery != other.RequiresRuntimeInterfaceDiscovery || Registrations.Length != other.Registrations.Length || ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length) @@ -1150,6 +1176,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator (ReflectionTypeMetadataName is null ? 0 : StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName)); + hashCode = (hashCode * 397) ^ RequiresRuntimeInterfaceDiscovery.GetHashCode(); foreach (var registration in Registrations) { hashCode = (hashCode * 397) ^ registration.GetHashCode(); From 73b63777ce2a610f5e95bd0f4f8dbeb92a504295 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:06:11 +0800 Subject: [PATCH 66/70] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现CqrsHandlerRegistryGenerator源代码生成器 - 减少运行时程序集反射扫描开销 - 支持IRequestHandler、INotificationHandler和IStreamRequestHandler接口 - 生成静态注册代码替代运行时动态发现 - 提供精确的运行时类型引用描述功能 - 实现泛型类型和数组类型的反射处理 - 添加日志记录和错误处理机制 - 支持跨程序集类型引用和内部类型反射查找 - 生成符合IServiceCollection的服务注册代码 --- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 49065f0c..3bd3571d 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -409,6 +409,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator case ITypeParameterSymbol: return false; default: + // Treat other Roslyn type kinds, such as dynamic or unresolved error types, as referenceable for now. + // If a real-world case proves unsafe, tighten this branch instead of broadening the named-type path above. return true; } } From 31b6285bbd41c4b65be4ead9d725a8c995c36060 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:06:20 +0800 Subject: [PATCH 67/70] =?UTF-8?q?test(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=88=86=E5=8F=91=E5=99=A8=E7=BC=93=E5=AD=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 验证相同消息类型重复分发时不会重复扩张服务类型与调用委托缓存 - 验证 request 调用委托会按响应类型分别缓存避免不同响应类型共用 object 结果桥接 - 实现通过反射读取 dispatcher 静态缓存字典的测试辅助方法 - 添加清空 dispatcher 静态缓存的方法避免跨用例共享进程级状态 - 创建多个测试数据模型和处理器用于验证不同的缓存场景 - 实现异步流消费方法确保建流路径被真实执行 --- GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 6d6e6a00..badd7490 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -12,6 +12,9 @@ namespace GFramework.Cqrs.Tests.Cqrs; [TestFixture] internal sealed class CqrsDispatcherCacheTests { + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// /// 初始化测试上下文。 /// @@ -42,9 +45,6 @@ internal sealed class CqrsDispatcherCacheTests _container = null; } - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 验证相同消息类型重复分发时,不会重复扩张服务类型与调用委托缓存。 /// @@ -165,6 +165,7 @@ internal sealed class CqrsDispatcherCacheTests GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers").Clear(); GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers").Clear(); GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers").Clear(); + GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(string), "Invokers").Clear(); } /// From 45bcffc6ee958f5b8cd2fa49be1b7ec762d99612 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:06:34 +0800 Subject: [PATCH 68/70] =?UTF-8?q?test(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS?= =?UTF-8?q?=20=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 MetadataReferenceTestBuilder 工具类用于构建内存元数据引用 - 实现 CreateFromSource 方法将源码编译为内存程序集并返回元数据引用 - 添加 GetRuntimeMetadataReferences 方法获取当前运行时可信平台程序集引用 - 创建 CqrsHandlerRegistryGeneratorTests 测试类验证 CQRS 处理器注册生成器功能 - 添加多种测试用例验证不同场景下的处理器注册行为 - 包含嵌套处理器、隐藏实现、数组类型参数、泛型类型定义等边界情况测试 - 实现混合直接注册和精确重建注册的测试验证 - 添加对外部基类保护类型处理器的支持测试 - 验证生成器优先处理隐藏处理器而不输出遗留回退标记的功能 --- .../Core/MetadataReferenceTestBuilder.cs | 13 +++++++++++-- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 7 +++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs b/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs index 173a88d9..45fd506a 100644 --- a/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs +++ b/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.IO; -using Microsoft.CodeAnalysis.CSharp; namespace GFramework.SourceGenerators.Tests.Core; @@ -9,6 +8,11 @@ namespace GFramework.SourceGenerators.Tests.Core; /// public static class MetadataReferenceTestBuilder { + // Reuse the runtime reference set across generator tests to avoid reparsing TRUSTED_PLATFORM_ASSEMBLIES + // for every in-memory compilation. + private static readonly Lazy> CachedRuntimeReferences = + new(CreateRuntimeMetadataReferences); + /// /// 将给定源码编译为内存程序集,并返回可供测试编译消费的元数据引用。 /// @@ -22,7 +26,7 @@ public static class MetadataReferenceTestBuilder params MetadataReference[] additionalReferences) { var syntaxTree = CSharpSyntaxTree.ParseText(source); - var references = GetRuntimeMetadataReferences() + var references = CachedRuntimeReferences.Value .Concat(additionalReferences) .ToImmutableArray(); var compilation = CSharpCompilation.Create( @@ -53,6 +57,11 @@ public static class MetadataReferenceTestBuilder /// /// 当前运行时可信平台程序集对应的元数据引用。 public static ImmutableArray GetRuntimeMetadataReferences() + { + return CachedRuntimeReferences.Value; + } + + private static ImmutableArray CreateRuntimeMetadataReferences() { var trustedPlatformAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))? .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index dc8c9abb..f51dde95 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1,7 +1,6 @@ using System.Reflection; using GFramework.SourceGenerators.Cqrs; using GFramework.SourceGenerators.Tests.Core; -using Microsoft.CodeAnalysis.CSharp; namespace GFramework.SourceGenerators.Tests.Cqrs; @@ -1155,7 +1154,11 @@ public class CqrsHandlerRegistryGeneratorTests var compilationErrors = updatedCompilation.GetDiagnostics() .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); - Assert.That(compilationErrors, Is.Empty, string.Join(Environment.NewLine, compilationErrors)); + Assert.That( + compilationErrors, + Is.Empty, + () => + $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); var runResult = driver.GetRunResult(); Assert.That(runResult.Results, Has.Length.EqualTo(1)); From 163077589e37b6360cd6b6d4e56ecb222933d5b2 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:08:03 +0800 Subject: [PATCH 69/70] =?UTF-8?q?refactor(tests):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E5=BC=95=E7=94=A8=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Microsoft.CodeAnalysis.CSharp 引用添加到 GlobalUsings.cs - 重新排列引用顺序以符合代码风格规范 --- GFramework.SourceGenerators.Tests/GlobalUsings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GFramework.SourceGenerators.Tests/GlobalUsings.cs b/GFramework.SourceGenerators.Tests/GlobalUsings.cs index 78c09fee..03266a66 100644 --- a/GFramework.SourceGenerators.Tests/GlobalUsings.cs +++ b/GFramework.SourceGenerators.Tests/GlobalUsings.cs @@ -20,4 +20,5 @@ global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.Text; global using Microsoft.CodeAnalysis.CSharp.Testing; global using Microsoft.CodeAnalysis.Testing; -global using NUnit.Framework; \ No newline at end of file +global using Microsoft.CodeAnalysis.CSharp; +global using NUnit.Framework; From 823be217799d08160ebb355be2bcc6f51a046d69 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:40:26 +0800 Subject: [PATCH 70/70] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E5=8F=91=E7=8E=B0=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=99=A8=E6=8E=A5=E5=8F=A3=E7=9A=84=E6=97=A5=E5=BF=97=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 HandlerCandidateAnalysis 结构中新增 RuntimeDiscoveredHandlerInterfaceLogNames 字段 - 为运行时发现的处理器接口创建日志名称收集器并存储显示名称 - 修改构造函数和属性以支持新的日志名称数组字段 - 更新相等性比较逻辑以包含运行时发现接口日志名称的比较 - 在生成的代码中添加注释显示剩余的运行时接口发现目标 - 更新单元测试验证生成的注释内容是否正确包含接口名称 --- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 4 ++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 46 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index f51dde95..2e321993 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -932,6 +932,10 @@ public class CqrsHandlerRegistryGeneratorTests generatedSource, Does.Contain( "var knownServiceTypes0 = new global::System.Collections.Generic.HashSet();")); + Assert.That( + generatedSource, + Does.Contain( + "// Remaining runtime interface discovery target: GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler")); Assert.That( generatedSource, Does.Contain( diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 3bd3571d..72ce6664 100644 --- a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -92,6 +92,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray.CreateBuilder(handlerInterfaces.Length); var preciseReflectedRegistrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var runtimeDiscoveredHandlerInterfaceLogNames = + ImmutableArray.CreateBuilder(handlerInterfaces.Length); var requiresRuntimeInterfaceDiscovery = false; foreach (var handlerInterface in handlerInterfaces) { @@ -128,6 +130,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator // 对这些边角场景保留“已知接口静态注册 + 剩余接口运行时补洞”的组合路径, // 避免单个未知接口把同实现上的其它已知注册全部拖回整实现反射发现。 requiresRuntimeInterfaceDiscovery = true; + runtimeDiscoveredHandlerInterfaceLogNames.Add(GetLogDisplayName(handlerInterface)); } return new HandlerCandidateAnalysis( @@ -137,7 +140,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator reflectedImplementationRegistrations.ToImmutable(), preciseReflectedRegistrations.ToImmutable(), canReferenceImplementation ? null : GetReflectionTypeMetadataName(type), - requiresRuntimeInterfaceDiscovery); + requiresRuntimeInterfaceDiscovery, + runtimeDiscoveredHandlerInterfaceLogNames.ToImmutable()); } private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, @@ -182,7 +186,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator candidate.ReflectedImplementationRegistrations, candidate.PreciseReflectedRegistrations, candidate.ReflectionTypeMetadataName, - candidate.RequiresRuntimeInterfaceDiscovery)); + candidate.RequiresRuntimeInterfaceDiscovery, + candidate.RuntimeDiscoveredHandlerInterfaceLogNames)); } registrations.Sort(static (left, right) => @@ -655,6 +660,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append(" var "); builder.Append(knownServiceTypesVariableName); builder.AppendLine(" = new global::System.Collections.Generic.HashSet();"); + foreach (var runtimeDiscoveredHandlerInterfaceLogName in registration + .RuntimeDiscoveredHandlerInterfaceLogNames) + { + builder.Append(" // Remaining runtime interface discovery target: "); + builder.Append(runtimeDiscoveredHandlerInterfaceLogName); + builder.AppendLine(); + } } foreach (var orderedRegistration in orderedRegistrations) @@ -1090,7 +1102,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray ReflectedImplementationRegistrations, ImmutableArray PreciseReflectedRegistrations, string? ReflectionTypeMetadataName, - bool RequiresRuntimeInterfaceDiscovery); + bool RequiresRuntimeInterfaceDiscovery, + ImmutableArray RuntimeDiscoveredHandlerInterfaceLogNames); private readonly struct HandlerCandidateAnalysis : IEquatable { @@ -1101,7 +1114,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray reflectedImplementationRegistrations, ImmutableArray preciseReflectedRegistrations, string? reflectionTypeMetadataName, - bool requiresRuntimeInterfaceDiscovery) + bool requiresRuntimeInterfaceDiscovery, + ImmutableArray runtimeDiscoveredHandlerInterfaceLogNames) { ImplementationTypeDisplayName = implementationTypeDisplayName; ImplementationLogName = implementationLogName; @@ -1110,6 +1124,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator PreciseReflectedRegistrations = preciseReflectedRegistrations; ReflectionTypeMetadataName = reflectionTypeMetadataName; RequiresRuntimeInterfaceDiscovery = requiresRuntimeInterfaceDiscovery; + RuntimeDiscoveredHandlerInterfaceLogNames = runtimeDiscoveredHandlerInterfaceLogNames; } public string ImplementationTypeDisplayName { get; } @@ -1126,6 +1141,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public bool RequiresRuntimeInterfaceDiscovery { get; } + public ImmutableArray RuntimeDiscoveredHandlerInterfaceLogNames { get; } + public bool Equals(HandlerCandidateAnalysis other) { if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, @@ -1136,7 +1153,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator RequiresRuntimeInterfaceDiscovery != other.RequiresRuntimeInterfaceDiscovery || Registrations.Length != other.Registrations.Length || ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || - PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length) + PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length || + RuntimeDiscoveredHandlerInterfaceLogNames.Length != + other.RuntimeDiscoveredHandlerInterfaceLogNames.Length) { return false; } @@ -1160,6 +1179,17 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } + for (var index = 0; index < RuntimeDiscoveredHandlerInterfaceLogNames.Length; index++) + { + if (!string.Equals( + RuntimeDiscoveredHandlerInterfaceLogNames[index], + other.RuntimeDiscoveredHandlerInterfaceLogNames[index], + StringComparison.Ordinal)) + { + return false; + } + } + return true; } @@ -1194,6 +1224,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode(); } + foreach (var runtimeDiscoveredHandlerInterfaceLogName in RuntimeDiscoveredHandlerInterfaceLogNames) + { + hashCode = (hashCode * 397) ^ + StringComparer.Ordinal.GetHashCode(runtimeDiscoveredHandlerInterfaceLogName); + } + return hashCode; } }