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 1/6] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0Microsoft=20?= =?UTF-8?q?DI=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 2/6] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0=20Microsoft?= =?UTF-8?q?=20DI=20=E5=AE=B9=E5=99=A8=E9=80=82=E9=85=8D=E5=99=A8=E5=92=8C?= =?UTF-8?q?=20CQRS=20=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 - 实现 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 3/6] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8=E5=92=8C?= =?UTF-8?q?=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 4/6] =?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 5/6] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0Microsoft=20?= =?UTF-8?q?DI=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 6/6] =?UTF-8?q?feat(ioc):=20=E6=B7=BB=E5=8A=A0Microsoft=20?= =?UTF-8?q?DI=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 {