Compare commits

...

12 Commits

Author SHA1 Message Date
gewuyou
ba6a358df1
Merge pull request #228 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-8
Refactor/Add CQRS dispatcher caching and refactor handler registration
2026-04-16 12:30:14 +08:00
GeWuYou
4951fb0254 feat(cqrs): 添加 CQRS 分发器和服务注册生成器
- 实现 CqrsDispatcher 类,支持请求/通知/流式请求的分发处理
- 添加进程级缓存机制,优化热路径中的反射和类型构造性能
- 实现上下文感知处理器的 CQRS 分发上下文注入功能
- 开发 CqrsHandlerRegistryGenerator 源代码生成器,减少运行时反射扫描
- 添加完整的单元测试验证缓存机制和服务类型注册功能
- 支持管道行为链处理和异步流式请求响应模式
2026-04-16 12:19:44 +08:00
GeWuYou
b07da252c4 refactor(cqrs): 优化并发处理能力
- 在 CqrsDispatcher 中添加 Concurrent 包引用以支持线程安全操作
- 在全局引用文件中增加 Concurrent 包引用,统一并发编程支持
- 为后续的并发处理逻辑改进奠定基础架构支持
2026-04-16 11:53:07 +08:00
GeWuYou
06f95db593 feat(cqrs): 添加CQRS调度器实现和改进处理器注册机制
- 实现GFramework自有CQRS运行时分发器,支持请求/通知/流式请求处理
- 添加进程级缓存机制优化反射调用性能,包括请求、通知、流水线调用委托缓存
- 重构CqrsHandlerRegistrar使用ReflectionFallbackMetadata替代字符串类型名
- 引入CqrsReflectionFallbackAttribute支持运行时补充反射扫描的处理器类型
- 添加完整的CQRS处理器注册单元测试,验证有序执行和容错行为
- 修复MicrosoftDiContainer中异常消息的格式化空白问题
- 实现上下文感知处理器的CQRS分发上下文注入功能
2026-04-16 11:36:31 +08:00
GeWuYou
391e3e9813 feat(cqrs): 添加CQRS处理器自动注册功能
- 实现CqrsHandlerRegistrar类,支持扫描并注册CQRS请求/通知/流式处理器
- 添加源码生成注册器优先策略,减少冷启动时的反射开销
- 实现运行时反射扫描回退机制,确保处理器注册的完整性
- 添加CqrsReflectionFallbackAttribute特性,标记需要运行时补充扫描的程序集
- 创建完整的单元测试套件,验证处理器注册顺序与容错行为
- 实现CqrsHandlerRegistryGenerator源码生成器,自动生成处理器注册代码
- 添加详细的日志记录与诊断功能,便于调试注册过程
- 实现类型安全的处理器映射验证与重复注册检测机制
2026-04-16 11:11:29 +08:00
gewuyou
21627c0381
Merge pull request #226 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-7
Refactor/cqrs architecture decoupling todo 7
2026-04-16 09:57:50 +08:00
GeWuYou
a4dfc78201 feat(ioc): 添加Microsoft DI容器适配器及测试
- 实现MicrosoftDiContainer类,包装IServiceProvider为IIocContainer接口
- 提供线程安全的依赖注入容器功能,支持单例、瞬态、作用域服务注册
- 添加RegisterSingleton、RegisterTransient、RegisterScoped等多种注册方法
- 实现RegisterPlurality方法支持一个实例注册到多个接口类型
- 添加CQRS相关注册功能,包括管道行为和处理器自动注册
- 实现Get、GetAll、GetRequired等服务解析方法
- 添加容器冻结机制,冻结后构建ServiceProvider提供服务解析
- 实现CreateScope方法支持服务作用域创建
- 添加完整的单元测试覆盖各种注册和解析场景
- 实现服务按优先级排序功能支持系统调度需求
2026-04-16 09:26:05 +08:00
GeWuYou
0d9d09bc4a feat(ioc): 添加Microsoft DI容器适配器及测试
- 实现MicrosoftDiContainer类作为IIocContainer接口的适配器
- 提供线程安全的依赖注入容器功能
- 支持单例、瞬态、作用域服务注册
- 实现CQRS处理器注册功能
- 添加服务工厂方法注册支持
- 实现按优先级排序的服务获取功能
- 添加完整的单元测试覆盖基本功能和边界情况
- 支持容器冻结和作用域创建功能
- 实现多样性实例注册到多个接口的功能
2026-04-16 09:14:27 +08:00
GeWuYou
00a1038d0a refactor(GFramework.Cqrs): 添加全局using引用System.Reflection
- 在GlobalUsings.cs文件中新增System.Reflection的全局引用
- 便于后续代码中直接使用Reflection相关功能
- 减少重复的using声明语句
2026-04-16 08:52:39 +08:00
GeWuYou
bc9336428e feat(cqrs): 添加 CQRS 处理器注册器和源码生成器
- 实现 CqrsHandlerRegistrar 类用于扫描并注册 CQRS 处理器
- 添加源码生成器自动生成 CQRS 处理器注册器减少反射开销
- 实现运行时回退机制在生成注册器不可用时使用反射扫描
- 添加完整的单元测试验证处理器注册顺序和容错行为
- 支持请求、通知和流式处理器的自动注册功能
- 实现稳定的处理器注册顺序保证跨环境一致性
- 添加详细的诊断日志记录注册过程和异常情况
2026-04-16 08:49:13 +08:00
GeWuYou
a7604de804 feat(ioc): 添加 Microsoft DI 容器适配器和 CQRS 运行时模块
- 实现 MicrosoftDiContainer 类,提供对 Microsoft.Extensions.DependencyInjection 的适配
- 添加线程安全的依赖注入容器功能,支持单例、瞬态和作用域服务注册
- 实现 CqrsRuntimeModule 模块,用于注册 CQRS 运行时组件
- 添加 CqrsRuntimeFactory 工厂类,提供 CQRS 运行时实现的创建入口
- 实现 DefaultCqrsRegistrationService,处理 CQRS 处理器的程序集注册
- 添加 CqrsTestRuntime 测试工具类,为测试环境提供 CQRS 运行时访问
- 支持多种注册方式包括实例注册、类型映射和工厂方法
- 实现服务获取、查询和生命周期管理功能
- 添加容器冻结机制以构建服务提供者
- 支持 CQRS 管道行为和处理器的批量注册功能
2026-04-16 08:37:40 +08:00
GeWuYou
1973fb2a60 feat(ioc): 添加Microsoft DI容器适配器和CQRS运行时模块
- 移除过时的Cqrs抽象引用
- 添加MicrosoftDiContainer实现IIocContainer接口
- 提供线程安全的依赖注入容器功能
- 支持单例、瞬态、作用域生命周期管理
- 实现CQRS请求管道行为注册功能
- 添加CqrsRuntimeModule服务模块
- 提供CQRS运行时实现和处理器注册器
- 扩展IArchitectureContext接口支持CQRS契约
2026-04-16 07:32:17 +08:00
21 changed files with 1714 additions and 270 deletions

View File

@ -18,7 +18,7 @@ namespace GFramework.Core.Abstractions.Architectures;
/// <para>新的 <c>GFramework.Cqrs.Abstractions.Cqrs</c> 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。</para>
/// <para>新功能优先使用 <see cref="SendRequestAsync{TResponse}(IRequest{TResponse},CancellationToken)" />、<see cref="SendAsync{TCommand}(TCommand,CancellationToken)" /> 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。</para>
/// </remarks>
public interface IArchitectureContext
public interface IArchitectureContext : ICqrsContext
{
/// <summary>
/// 获取指定类型的服务实例

View File

@ -1,52 +1,16 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Cqrs.Abstractions.Cqrs;
using System.ComponentModel;
namespace GFramework.Core.Abstractions.Cqrs;
/// <summary>
/// 定义架构上下文使用的 CQRS runtime seam。
/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦,
/// 使 <see cref="IArchitectureContext" /> 不再直接依赖某个固定的 runtime 类型。
/// 提供旧 <c>GFramework.Core.Abstractions.Cqrs</c> 命名空间下的 CQRS runtime 兼容别名。
/// </summary>
public interface ICqrsRuntime
/// <remarks>
/// 正式 runtime seam 已迁移到 <see cref="GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime" />
/// 但当前仍保留该接口以避免立即打断历史公开路径与既有二进制引用。
/// 新代码应优先依赖 <c>GFramework.Cqrs.Abstractions.Cqrs</c> 下的正式契约。
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface ICqrsRuntime : GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime
{
/// <summary>
/// 发送请求并返回响应。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。</param>
/// <param name="request">要分发的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
ValueTask<TResponse> SendAsync<TResponse>(
IArchitectureContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default);
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示通知分发完成的值任务。</returns>
ValueTask PublishAsync<TNotification>(
IArchitectureContext context,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification;
/// <summary>
/// 创建流式请求的异步响应序列。
/// </summary>
/// <typeparam name="TResponse">流元素类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>按需生成的异步响应序列。</returns>
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IArchitectureContext context,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default);
}

View File

@ -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;

View File

@ -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<ICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<LegacyICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
Assert.That(_container.Get<ICqrsRuntime>(), Is.SameAs(_container.Get<LegacyICqrsRuntime>()));
CqrsTestRuntime.RegisterHandlers(_container);
Assert.That(_container.GetAll<ICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<LegacyICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
Assert.That(_container.Get<ICqrsRuntime>(), Is.SameAs(_container.Get<LegacyICqrsRuntime>()));
}
/// <summary>
@ -245,6 +249,46 @@ public class MicrosoftDiContainerTests
Assert.That(results.Count, Is.EqualTo(0));
}
/// <summary>
/// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复,
/// 但会保留同一服务类型的重复显式注册。
/// </summary>
[Test]
public void GetAll_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases()
{
var instance = new AliasAwareService();
_container.Register<IPrimaryAliasService>(instance);
_container.Register<IPrimaryAliasService>(instance);
_container.Register<ISecondaryAliasService>(instance);
var results = _container.GetAll<ISharedAliasService>();
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results[0], Is.SameAs(instance));
Assert.That(results[1], Is.SameAs(instance));
}
/// <summary>
/// 测试非泛型 GetAll 在容器未冻结时与泛型重载保持相同的别名去重语义。
/// </summary>
[Test]
public void
GetAll_Type_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases()
{
var instance = new AliasAwareService();
_container.Register<IPrimaryAliasService>(instance);
_container.Register<IPrimaryAliasService>(instance);
_container.Register<ISecondaryAliasService>(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));
}
/// <summary>
/// 测试获取排序后的所有实例的功能
/// </summary>
@ -357,6 +401,17 @@ public class MicrosoftDiContainerTests
Is.True);
}
/// <summary>
/// 测试当程序集集合中包含空元素时CQRS handler 注册入口会在委托给注册服务前直接失败。
/// </summary>
[Test]
public void RegisterCqrsHandlersFromAssemblies_WithNullAssemblyItem_Should_ThrowArgumentNullException()
{
var assemblies = new Assembly[] { typeof(DeterministicOrderNotification).Assembly, null! };
Assert.Throws<ArgumentNullException>(() => _container.RegisterCqrsHandlersFromAssemblies(assemblies));
}
/// <summary>
/// 测试冻结容器以防止进一步注册的功能
/// </summary>
@ -712,6 +767,28 @@ public interface IMixedService
string? Name { get; set; }
}
/// <summary>
/// 用于验证未冻结查询路径中的服务别名去重行为。
/// </summary>
public interface ISharedAliasService;
/// <summary>
/// 主服务别名接口。
/// </summary>
public interface IPrimaryAliasService : ISharedAliasService;
/// <summary>
/// 次级兼容别名接口。
/// </summary>
public interface ISecondaryAliasService : ISharedAliasService;
/// <summary>
/// 同时实现多个别名接口的测试服务。
/// </summary>
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
{
}
/// <summary>
/// 实现优先级的服务
/// </summary>

View File

@ -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
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <returns>查询结果</returns>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
}
@ -216,8 +215,7 @@ public class ArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含查询结果的ValueTask</returns>
public async ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
@ -354,7 +352,7 @@ public class ArchitectureContext : IArchitectureContext
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含命令执行结果的ValueTask</returns>
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(command);
@ -393,7 +391,7 @@ public class ArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <returns>命令执行结果</returns>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
}

View File

@ -4,8 +4,8 @@ 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;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Ioc;
@ -35,6 +35,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
#endregion
/// <summary>
/// 记录某个实例在未冻结查询中可见的服务类型分组信息。
/// </summary>
/// <param name="ServiceType">当前分组对应的服务类型。</param>
/// <param name="Count">该服务类型下的描述符数量。</param>
/// <param name="FirstIndex">该服务类型首次出现的位置,用于稳定打破并列。</param>
private sealed record VisibleServiceTypeGroup(Type ServiceType, int Count, int FirstIndex);
#region Fields
/// <summary>
@ -57,12 +65,6 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
private readonly HashSet<object> _registeredInstances = [];
/// <summary>
/// 已接入 CQRS handler 注册流程的程序集键集合。
/// 使用稳定字符串键而不是 Assembly 引用本身,以避免默认路径和显式扩展路径使用不同 Assembly 对象时重复注册。
/// </summary>
private readonly HashSet<string> _registeredCqrsHandlerAssemblyKeys = new(StringComparer.Ordinal);
/// <summary>
/// 日志记录器,用于记录容器操作日志
/// </summary>
@ -397,35 +399,22 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 中存在 <see langword="null" /> 元素。</exception>
/// <exception cref="InvalidOperationException"> 容器已冻结,无法继续注册 CQRS 处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
var assemblyArray = assemblies.ToArray();
foreach (var assembly in assemblyArray)
{
ArgumentNullException.ThrowIfNull(assembly);
}
_lock.EnterWriteLock();
try
{
ThrowIfFrozen();
var processedAssemblyKeys = new HashSet<string>(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(assemblyArray);
}
finally
{
@ -456,22 +445,22 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
#region Get
/// <summary>
/// 获取当前容器中已注册的 CQRS 处理器注册器。
/// 获取当前容器中已注册的 CQRS 程序集注册协调器。
/// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定,
/// 避免在容器未冻结前依赖完整的服务提供者构建流程。
/// </summary>
/// <returns>已注册的 CQRS 处理器注册器实例。</returns>
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 处理器注册器实例时抛出。</exception>
private ICqrsHandlerRegistrar ResolveCqrsHandlerRegistrar()
/// <returns>已注册的 CQRS 程序集注册协调器实例。</returns>
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 程序集注册协调器实例时抛出。</exception>
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);
}
@ -618,29 +607,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<T>();
foreach (var descriptor in registeredServices)
{
if (descriptor.ImplementationInstance is T instance)
{
result.Add(instance);
}
else if (descriptor.ImplementationFactory != null)
{
// 在未冻结状态下无法调用工厂方法,跳过
}
else if (descriptor.ImplementationType != null)
{
// 在未冻结状态下无法创建实例,跳过
}
}
return result;
return CollectRegisteredImplementationInstances(typeof(T)).Cast<T>().ToList();
}
var services = _provider!.GetServices<T>().ToList();
@ -658,37 +625,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
/// <param name="type">服务类型</param>
/// <returns>只读的服务实例列表</returns>
/// <exception cref="InvalidOperationException">当容器未冻结时抛出</exception>
/// <exception cref="ArgumentNullException">当 <paramref name="type" /> 为 <see langword="null" /> 时抛出</exception>
public IReadOnlyList<object> 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<object>();
foreach (var descriptor in registeredServices)
{
if (descriptor.ImplementationInstance != null)
{
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();
@ -701,6 +648,108 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 在容器未冻结时,从服务描述符中收集当前可直接观察到的实例绑定。
/// </summary>
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
/// <returns>按当前未冻结语义可见的实例列表。</returns>
/// <remarks>
/// 该方法只读取 <see cref="ServiceDescriptor.ImplementationInstance" />,因为未冻结路径不会主动执行工厂方法,
/// 也不会提前构造 <see cref="ServiceDescriptor.ImplementationType" />。
/// 若同一实例同时经由多个可赋值的 <see cref="ServiceDescriptor.ServiceType" /> 暴露,
/// 这里会把它视为兼容别名并只保留一个规范服务类型对应的结果;
/// 但同一 <see cref="ServiceDescriptor.ServiceType" /> 的重复显式注册仍会完整保留,以维持注册顺序和多次注册语义。
/// </remarks>
private List<object> 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<object>();
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;
}
/// <summary>
/// 为每个可见实例选择一个规范服务类型,避免同一实例因兼容别名重复出现在未冻结查询结果中。
/// </summary>
/// <param name="matchingDescriptors">已按请求类型过滤过的服务描述符集合。</param>
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
/// <returns>实例到其规范服务类型的映射。</returns>
private static Dictionary<object, Type> BuildPreferredVisibleServiceTypes(
IReadOnlyList<ServiceDescriptor> matchingDescriptors,
Type requestedServiceType)
{
var preferredServiceTypes = new Dictionary<object, Type>(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;
}
/// <summary>
/// 在“同一实例被多个服务类型暴露”的场景下,选择未冻结查询结果应保留的规范服务类型。
/// </summary>
/// <param name="descriptorsForInstance">引用同一实例的服务描述符。</param>
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
/// <returns>应在结果中保留的服务类型。</returns>
private static Type SelectPreferredVisibleServiceType(
IEnumerable<ServiceDescriptor> 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;
}
/// <summary>
/// 获取并排序指定泛型类型的所有服务实例
/// 主要用于系统调度场景
@ -827,7 +876,6 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_registeredCqrsHandlerAssemblyKeys.Clear();
_provider = null;
_logger.Info("Container cleared");
}
@ -899,16 +947,5 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 生成 CQRS handler 注册用的稳定程序集键。
/// 该键需要同时兼顾真实程序集与测试中使用的 mocked Assembly避免仅靠引用比较导致重复接入。
/// </summary>
/// <param name="assembly">目标程序集。</param>
/// <returns>稳定的程序集标识字符串。</returns>
private static string GetCqrsAssemblyRegistrationKey(Assembly assembly)
{
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
}
#endregion
}

View File

@ -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,10 +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<ICqrsRuntime>(CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger));
container.Register<ICqrsHandlerRegistrar>(
CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger));
container.Register(runtime);
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime);
container.Register<ICqrsHandlerRegistrar>(registrar);
container.Register<ICqrsRegistrationService>(
CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger));
}
/// <summary>

View File

@ -0,0 +1,13 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义 CQRS runtime 在分发期间携带的最小上下文标记。
/// </summary>
/// <remarks>
/// 该接口当前刻意保持为轻量 marker seam只用于让 <see cref="ICqrsRuntime" /> 从
/// <c>GFramework.Core.Abstractions</c> 的 <c>IArchitectureContext</c> 解耦。
/// 运行时实现仍可在需要时识别更具体的上下文类型,并对现有 <c>IContextAware</c> 处理器执行兼容注入。
/// </remarks>
public interface ICqrsContext
{
}

View File

@ -0,0 +1,81 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义架构上下文使用的 CQRS runtime seam。
/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦,
/// 使 CQRS runtime 契约可独立归属到 <c>GFramework.Cqrs.Abstractions</c>。
/// </summary>
public interface ICqrsRuntime
{
/// <summary>
/// 发送请求并返回响应。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="request">要分发的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="context" /> 或 <paramref name="request" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// 当前上下文无法满足运行时要求,例如未找到对应请求处理器,或请求处理链中的
/// <c>IContextAware</c> 对象需要 <c>IArchitectureContext</c> 但当前 <paramref name="context" /> 不提供该能力。
/// </exception>
/// <remarks>
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
/// </remarks>
ValueTask<TResponse> SendAsync<TResponse>(
ICqrsContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default);
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示通知分发完成的值任务。</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="context" /> 或 <paramref name="notification" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// 已解析到的通知处理器需要框架级上下文注入,但当前 <paramref name="context" /> 不提供
/// <c>IArchitectureContext</c> 能力。
/// </exception>
/// <remarks>
/// 默认实现允许零处理器场景静默完成;只有在处理器注入前置条件不满足时才会抛出异常。
/// </remarks>
ValueTask PublishAsync<TNotification>(
ICqrsContext context,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification;
/// <summary>
/// 创建流式请求的异步响应序列。
/// </summary>
/// <typeparam name="TResponse">流元素类型。</typeparam>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>按需生成的异步响应序列。</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="context" /> 或 <paramref name="request" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// 当前上下文无法满足运行时要求,例如未找到对应流式处理器,或流式处理链中的
/// <c>IContextAware</c> 对象需要 <c>IArchitectureContext</c> 但当前 <paramref name="context" /> 不提供该能力。
/// </exception>
/// <remarks>
/// 返回的异步序列在枚举前通常已完成处理器解析与上下文注入,
/// 因此调用方应把 <paramref name="context" /> 视为整个枚举生命周期内的必需依赖。
/// </remarks>
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
ICqrsContext context,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,232 @@
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;
/// <summary>
/// 验证 CQRS dispatcher 会缓存热路径中的服务类型构造结果。
/// </summary>
[TestFixture]
internal sealed class CqrsDispatcherCacheTests
{
/// <summary>
/// 初始化测试上下文。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
CqrsTestRuntime.RegisterHandlers(
_container,
typeof(CqrsDispatcherCacheTests).Assembly,
typeof(ArchitectureContext).Assembly);
_container.Freeze();
_context = new ArchitectureContext(_container);
}
/// <summary>
/// 清理测试上下文引用。
/// </summary>
[TearDown]
public void TearDown()
{
_context = null;
_container = null;
}
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 验证相同消息类型重复分发时,不会重复扩张服务类型缓存。
/// </summary>
[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 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 + 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));
});
}
/// <summary>
/// 通过反射读取 dispatcher 的静态缓存字典。
/// </summary>
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.");
}
/// <summary>
/// 消费整个异步流,确保建流路径被真实执行。
/// </summary>
private static async Task DrainAsync<T>(IAsyncEnumerable<T> stream)
{
await foreach (var _ in stream)
{
}
}
}
/// <summary>
/// 用于验证 request 服务类型缓存的测试请求。
/// </summary>
internal sealed record DispatcherCacheRequest : IRequest<int>;
/// <summary>
/// 用于验证 notification 服务类型缓存的测试通知。
/// </summary>
internal sealed record DispatcherCacheNotification : INotification;
/// <summary>
/// 用于验证 stream 服务类型缓存的测试请求。
/// </summary>
internal sealed record DispatcherCacheStreamRequest : IStreamRequest<int>;
/// <summary>
/// 用于验证 pipeline invoker 缓存的测试请求。
/// </summary>
internal sealed record DispatcherPipelineCacheRequest : IRequest<int>;
/// <summary>
/// 处理 <see cref="DispatcherCacheRequest" />。
/// </summary>
internal sealed class DispatcherCacheRequestHandler : IRequestHandler<DispatcherCacheRequest, int>
{
/// <summary>
/// 返回固定结果,供缓存测试验证 dispatcher 请求路径。
/// </summary>
public ValueTask<int> Handle(DispatcherCacheRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(1);
}
}
/// <summary>
/// 处理 <see cref="DispatcherCacheNotification" />。
/// </summary>
internal sealed class DispatcherCacheNotificationHandler : INotificationHandler<DispatcherCacheNotification>
{
/// <summary>
/// 消费通知,不执行额外副作用。
/// </summary>
public ValueTask Handle(DispatcherCacheNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 处理 <see cref="DispatcherCacheStreamRequest" />。
/// </summary>
internal sealed class DispatcherCacheStreamHandler : IStreamRequestHandler<DispatcherCacheStreamRequest, int>
{
/// <summary>
/// 返回一个最小流,供缓存测试命中 stream 分发路径。
/// </summary>
public async IAsyncEnumerable<int> Handle(
DispatcherCacheStreamRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return 1;
await Task.CompletedTask;
}
}
/// <summary>
/// 处理 <see cref="DispatcherPipelineCacheRequest" />。
/// </summary>
internal sealed class DispatcherPipelineCacheRequestHandler : IRequestHandler<DispatcherPipelineCacheRequest, int>
{
/// <summary>
/// 返回固定结果,供 pipeline 缓存测试使用。
/// </summary>
public ValueTask<int> Handle(DispatcherPipelineCacheRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(2);
}
}
/// <summary>
/// 为 <see cref="DispatcherPipelineCacheRequest" /> 提供最小 pipeline 行为,
/// 用于命中 dispatcher 的 pipeline invoker 缓存分支。
/// </summary>
internal sealed class DispatcherPipelineCacheBehavior : IPipelineBehavior<DispatcherPipelineCacheRequest, int>
{
/// <summary>
/// 直接转发到下一个处理器。
/// </summary>
public ValueTask<int> Handle(
DispatcherPipelineCacheRequest request,
MessageHandlerDelegate<DispatcherPipelineCacheRequest, int> next,
CancellationToken cancellationToken)
{
return next(request, cancellationToken);
}
}

View File

@ -13,6 +13,9 @@ namespace GFramework.Cqrs.Tests.Cqrs;
[TestFixture]
internal sealed class CqrsHandlerRegistrarTests
{
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 初始化测试容器并重置共享状态。
/// </summary>
@ -42,9 +45,6 @@ internal sealed class CqrsHandlerRegistrarTests
DeterministicNotificationHandlerState.Reset();
}
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
/// </summary>
@ -188,6 +188,110 @@ internal sealed class CqrsHandlerRegistrarTests
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers
/// 而不是重新枚举整个程序集的类型列表。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Targeted_Type_Lookups_For_Reflection_Fallback_Without_Duplicates()
{
var generatedAssembly = new Mock<Assembly>();
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(
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);
var registrations = container.GetServicesUnsafe
.Where(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
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.Once);
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
}
/// <summary>
/// 验证手写 fallback metadata 直接提供 handler 类型时,运行时会复用这些类型,
/// 而不会再通过程序集名称查找或整程序集扫描补齐映射。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Direct_Fallback_Types_Without_GetType_Or_GetTypes()
{
var generatedAssembly = new Mock<Assembly>();
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<GeneratedRegistryNotification>) &&
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);
}
}
/// <summary>
@ -337,3 +441,52 @@ internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistr
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}
/// <summary>
/// 用于验证“生成注册器 + reflection fallback”组合路径的私有嵌套处理器容器。
/// </summary>
internal sealed class ReflectionFallbackNotificationContainer
{
/// <summary>
/// 获取仅能通过反射补扫接入的私有嵌套处理器类型。
/// </summary>
public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler);
private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler
: INotificationHandler<GeneratedRegistryNotification>
{
/// <summary>
/// 处理测试通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
}
/// <summary>
/// 模拟局部生成注册器场景中,仅注册“可由生成代码直接引用”的那部分 handlers。
/// </summary>
internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将生成路径可见的通知处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}

View File

@ -0,0 +1,71 @@
namespace GFramework.Cqrs;
/// <summary>
/// 标记程序集中的 CQRS 生成注册器仍需要运行时补充反射扫描。
/// </summary>
/// <remarks>
/// 该特性通常由源码生成器自动添加到消费端程序集。
/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描,
/// 以覆盖那些生成代码无法直接引用的 handler 类型。
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
/// <summary>
/// 初始化 <see cref="CqrsReflectionFallbackAttribute" />,保留旧版“仅标记需要补扫”的语义。
/// </summary>
public CqrsReflectionFallbackAttribute()
{
FallbackHandlerTypeNames = [];
FallbackHandlerTypes = [];
}
/// <summary>
/// 初始化 <see cref="CqrsReflectionFallbackAttribute" />。
/// </summary>
/// <param name="fallbackHandlerTypeNames">
/// 需要运行时补充反射注册的处理器类型全名。
/// 当该清单为空时,运行时会回退到整程序集扫描,以兼容旧版 marker 语义。
/// </param>
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();
FallbackHandlerTypes = [];
}
/// <summary>
/// 初始化 <see cref="CqrsReflectionFallbackAttribute" />。
/// </summary>
/// <param name="fallbackHandlerTypes">
/// 需要运行时补充反射注册的处理器类型。
/// 该重载适合手写或第三方程序集显式声明可直接引用的 fallback handlers
/// 避免再通过字符串名称回查程序集元数据。
/// </param>
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();
}
/// <summary>
/// 获取需要运行时补充反射注册的处理器类型全名集合。
/// </summary>
public IReadOnlyList<string> FallbackHandlerTypeNames { get; }
/// <summary>
/// 获取可直接供运行时补充反射注册的处理器类型集合。
/// </summary>
public IReadOnlyList<Type> FallbackHandlerTypes { get; }
}

View File

@ -1,4 +1,3 @@
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
@ -48,4 +47,21 @@ public static class CqrsRuntimeFactory
return new DefaultCqrsHandlerRegistrar(container, logger);
}
/// <summary>
/// 创建默认的 CQRS 程序集注册协调器。
/// </summary>
/// <param name="registrar">底层 handler 注册器。</param>
/// <param name="logger">用于注册阶段诊断的日志器。</param>
/// <returns>默认 CQRS 程序集注册协调器。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="registrar" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
/// </exception>
public static ICqrsRegistrationService CreateRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger)
{
ArgumentNullException.ThrowIfNull(registrar);
ArgumentNullException.ThrowIfNull(logger);
return new DefaultCqrsRegistrationService(registrar, logger);
}
}

View File

@ -3,5 +3,7 @@ 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;
global using System.Collections.Concurrent;

View File

@ -0,0 +1,19 @@
using System.Reflection;
namespace GFramework.Cqrs;
/// <summary>
/// 协调 CQRS 处理器程序集的接入流程。
/// </summary>
/// <remarks>
/// 该服务封装“程序集去重 + 生成注册器优先 + 反射回退”的默认接入语义,
/// 让 <c>GFramework.Core</c> 的容器层只保留公开入口,而不再直接维护 CQRS handler 注册细节。
/// </remarks>
public interface ICqrsRegistrationService
{
/// <summary>
/// 注册一个或多个程序集中的 CQRS 处理器。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
void RegisterHandlers(IEnumerable<Assembly> assemblies);
}

View File

@ -1,17 +1,15 @@
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;
/// <summary>
/// GFramework 自有 CQRS 运行时分发器。
/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前架构上下文。
/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前 CQRS 分发上下文。
/// </summary>
internal sealed class CqrsDispatcher(
IIocContainer container,
@ -30,19 +28,44 @@ internal sealed class CqrsDispatcher(
// 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。
private static readonly ConcurrentDictionary<Type, NotificationInvoker> NotificationInvokers = new();
// 进程级缓存:缓存通知处理器服务类型,避免每次发布都重复 MakeGenericType。
private static readonly ConcurrentDictionary<Type, Type> 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();
// 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。
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)!;
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="context">当前 CQRS 分发上下文,用于上下文感知处理器注入。</param>
/// <param name="notification">通知对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
public async ValueTask PublishAsync<TNotification>(
IArchitectureContext context,
ICqrsContext context,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification
@ -51,7 +74,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)
@ -75,12 +100,12 @@ internal sealed class CqrsDispatcher(
/// 发送请求并返回结果。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="context">当前 CQRS 分发上下文,用于上下文感知处理器注入。</param>
/// <param name="request">请求对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
public async ValueTask<TResponse> SendAsync<TResponse>(
IArchitectureContext context,
ICqrsContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
@ -88,14 +113,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);
@ -122,12 +151,12 @@ internal sealed class CqrsDispatcher(
/// 创建流式请求并返回异步响应序列。
/// </summary>
/// <typeparam name="TResponse">响应元素类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="context">当前 CQRS 分发上下文,用于上下文感知处理器注入。</param>
/// <param name="request">流式请求对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应序列。</returns>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IArchitectureContext context,
ICqrsContext context,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
@ -135,7 +164,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}.");
@ -150,14 +181,20 @@ internal sealed class CqrsDispatcher(
}
/// <summary>
/// 为上下文感知处理器注入当前架构上下文。
/// 为上下文感知处理器注入当前 CQRS 分发上下文。
/// </summary>
/// <param name="handler">处理器实例。</param>
/// <param name="context">当前架构上下文。</param>
private static void PrepareHandler(object handler, IArchitectureContext context)
/// <param name="context">当前 CQRS 分发上下文。</param>
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);
}
}
/// <summary>
@ -165,8 +202,7 @@ internal sealed class CqrsDispatcher(
/// </summary>
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);
}
@ -176,8 +212,7 @@ internal sealed class CqrsDispatcher(
/// </summary>
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);
}
@ -187,8 +222,7 @@ internal sealed class CqrsDispatcher(
/// </summary>
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);
}
@ -198,8 +232,7 @@ internal sealed class CqrsDispatcher(
/// </summary>
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);
}
@ -287,4 +320,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);
}

View File

@ -1,4 +1,3 @@
using System.Reflection;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
@ -32,10 +31,16 @@ internal static class CqrsHandlerRegistrar
.Distinct()
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
{
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
var generatedRegistrationResult =
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
if (generatedRegistrationResult is { UsedGeneratedRegistry: true, RequiresReflectionFallback: false })
continue;
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
RegisterAssemblyHandlers(
container.GetServicesUnsafe,
assembly,
logger,
generatedRegistrationResult.ReflectionFallbackMetadata);
}
}
@ -45,8 +50,11 @@ internal static class CqrsHandlerRegistrar
/// <param name="services">目标服务集合。</param>
/// <param name="assembly">当前要处理的程序集。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>当成功使用生成注册器时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
/// <returns>生成注册器的使用结果。</returns>
private static GeneratedRegistrationResult TryRegisterGeneratedHandlers(
IServiceCollection services,
Assembly assembly,
ILogger logger)
{
var assemblyName = GetAssemblySortKey(assembly);
@ -62,7 +70,7 @@ internal static class CqrsHandlerRegistrar
.ToList();
if (registryTypes.Count == 0)
return false;
return GeneratedRegistrationResult.NoGeneratedRegistry();
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
@ -71,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 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 +106,24 @@ internal static class CqrsHandlerRegistrar
registry.Register(services, logger);
}
return true;
var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger);
if (reflectionFallbackMetadata is not null)
{
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
return GeneratedRegistrationResult.FullyHandled();
}
catch (Exception exception)
{
@ -106,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 false;
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
}
/// <summary>
/// 注册单个程序集里的所有 CQRS 处理器映射。
/// </summary>
private static void RegisterAssemblyHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
private static void RegisterAssemblyHandlers(
IServiceCollection services,
Assembly assembly,
ILogger logger,
ReflectionFallbackMetadata? reflectionFallbackMetadata)
{
foreach (var implementationType in GetLoadableTypes(assembly, logger).Where(IsConcreteHandlerType))
foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata)
.Where(IsConcreteHandlerType))
{
var handlerInterfaces = implementationType
.GetInterfaces()
@ -128,6 +158,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);
@ -137,6 +174,87 @@ internal static class CqrsHandlerRegistrar
}
}
/// <summary>
/// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。
/// </summary>
private static IReadOnlyList<Type> GetCandidateHandlerTypes(
Assembly assembly,
ILogger logger,
ReflectionFallbackMetadata? reflectionFallbackMetadata)
{
return reflectionFallbackMetadata is { HasExplicitTypes: true }
? reflectionFallbackMetadata.Types
: GetLoadableTypes(assembly, logger);
}
/// <summary>
/// 获取生成注册器要求运行时继续补充反射扫描的 handler 元数据。
/// </summary>
private static ReflectionFallbackMetadata? GetReflectionFallbackMetadata(
Assembly assembly,
ILogger logger)
{
var assemblyName = GetAssemblySortKey(assembly);
var fallbackAttributes = assembly
.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)
.OfType<CqrsReflectionFallbackAttribute>()
.ToList();
if (fallbackAttributes.Count == 0)
return null;
var resolvedTypes = new List<Type>();
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))
{
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 new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
}
/// <summary>
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
/// </summary>
@ -202,6 +320,21 @@ internal static class CqrsHandlerRegistrar
definition == typeof(IStreamRequestHandler<,>);
}
/// <summary>
/// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。
/// </summary>
private static bool IsHandlerMappingAlreadyRegistered(
IServiceCollection services,
Type handlerInterface,
Type implementationType)
{
// 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。
// 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。
return services.Any(descriptor =>
descriptor.ServiceType == handlerInterface &&
descriptor.ImplementationType == implementationType);
}
/// <summary>
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
/// </summary>
@ -217,4 +350,44 @@ internal static class CqrsHandlerRegistrar
{
return type.FullName ?? type.Name;
}
private readonly record struct GeneratedRegistrationResult(
bool UsedGeneratedRegistry,
bool RequiresReflectionFallback,
ReflectionFallbackMetadata? ReflectionFallbackMetadata)
{
public static GeneratedRegistrationResult NoGeneratedRegistry()
{
return new GeneratedRegistrationResult(
UsedGeneratedRegistry: false,
RequiresReflectionFallback: false,
ReflectionFallbackMetadata: null);
}
public static GeneratedRegistrationResult FullyHandled()
{
return new GeneratedRegistrationResult(
UsedGeneratedRegistry: true,
RequiresReflectionFallback: false,
ReflectionFallbackMetadata: null);
}
public static GeneratedRegistrationResult WithReflectionFallback(
ReflectionFallbackMetadata reflectionFallbackMetadata)
{
ArgumentNullException.ThrowIfNull(reflectionFallbackMetadata);
return new GeneratedRegistrationResult(
UsedGeneratedRegistry: true,
RequiresReflectionFallback: true,
ReflectionFallbackMetadata: reflectionFallbackMetadata);
}
}
private sealed class ReflectionFallbackMetadata(IReadOnlyList<Type> types)
{
public IReadOnlyList<Type> Types { get; } = types ?? throw new ArgumentNullException(nameof(types));
public bool HasExplicitTypes => Types.Count > 0;
}
}

View File

@ -0,0 +1,62 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Internal;
/// <summary>
/// 默认的 CQRS 程序集注册协调器。
/// </summary>
/// <remarks>
/// 该实现把“按稳定程序集键去重”和“委托给 handler registrar 执行实际映射注册”收敛到 CQRS runtime 内部,
/// 避免外层容器继续了解 handler 注册流水线的内部结构。
/// <para>
/// 该类型不是线程安全的。调用方应在外部同步边界内访问 <see cref="RegisterHandlers" />
/// 例如由容器写锁串行化程序集注册流程。
/// </para>
/// </remarks>
internal sealed class DefaultCqrsRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger)
: ICqrsRegistrationService
{
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
private readonly HashSet<string> _registeredAssemblyKeys = new(StringComparer.Ordinal);
private readonly ICqrsHandlerRegistrar _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
/// <summary>
/// 注册指定程序集中的 CQRS handlers。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
public void RegisterHandlers(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
var processedAssemblyKeys = new HashSet<string>(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);
}
}
/// <summary>
/// 生成稳定程序集键,避免相同程序集被不同 <see cref="Assembly" /> 实例重复接入。
/// </summary>
/// <param name="assembly">目标程序集。</param>
/// <returns>稳定的程序集标识。</returns>
private static string GetAssemblyRegistrationKey(Assembly assembly)
{
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
}
}

View File

@ -10,6 +10,138 @@ namespace GFramework.SourceGenerators.Tests.Cqrs;
[TestFixture]
public class CqrsHandlerRegistryGeneratorTests
{
private const string HiddenNestedHandlerSelfRegistrationExpected = """
// <auto-generated />
#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<global::TestApp.VisibleRequest, string>),
typeof(global::TestApp.VisibleHandler));
logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<TestApp.VisibleRequest, string>.");
}
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();
}
}
""";
/// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary>
@ -61,6 +193,12 @@ public class CqrsHandlerRegistryGeneratorTests
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
}
}
namespace TestApp
@ -120,10 +258,174 @@ public class CqrsHandlerRegistryGeneratorTests
}
/// <summary>
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册,
/// 不再依赖程序集级 fallback marker。
/// </summary>
[Test]
public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_Handler()
public async Task
Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_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<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
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(params string[] fallbackHandlerTypeNames) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleRequest() : IRequest<string>;
public sealed class Container
{
private sealed record HiddenRequest() : IRequest<string>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, string> { }
}
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string> { }
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。
/// </summary>
[Test]
public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_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<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
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() { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record VisibleRequest() : IRequest<string>;
public sealed class Container
{
private sealed record HiddenRequest() : IRequest<string>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, string> { }
}
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string> { }
}
""";
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性,
/// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。
/// </summary>
[Test]
public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable()
{
const string source = """
using System;
@ -188,16 +490,9 @@ public class CqrsHandlerRegistryGeneratorTests
}
""";
var test = new CSharpSourceGeneratorTest<CqrsHandlerRegistryGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
await test.RunAsync();
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>

View File

@ -28,8 +28,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
/// <inheritdoc />
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 +39,22 @@ 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);
}
private static bool IsHandlerCandidate(SyntaxNode node)
@ -82,16 +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<HandlerRegistrationSpec>.Empty,
true);
GetReflectionTypeMetadataName(type));
}
var implementationLogName = GetLogDisplayName(type);
var registrations = ImmutableArray.CreateBuilder<HandlerRegistrationSpec>(handlerInterfaces.Length);
foreach (var handlerInterface in handlerInterfaces)
{
@ -104,36 +111,34 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return new HandlerCandidateAnalysis(
implementationTypeDisplayName,
implementationLogName,
registrations.MoveToImmutable(),
false);
null);
}
private static void Execute(SourceProductionContext context, bool generationEnabled,
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
ImmutableArray<HandlerCandidateAnalysis?> candidates)
{
if (!generationEnabled)
if (!generationEnvironment.GenerationEnabled)
return;
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
var registrations = CollectRegistrations(candidates);
// 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));
context.AddSource(
HintName,
GenerateSource(registrations));
}
private static List<HandlerRegistrationSpec> CollectRegistrations(
ImmutableArray<HandlerCandidateAnalysis?> candidates,
out bool hasUnsupportedConcreteHandler)
private static List<ImplementationRegistrationSpec> CollectRegistrations(
ImmutableArray<HandlerCandidateAnalysis?> candidates)
{
var registrations = new List<HandlerRegistrationSpec>();
hasUnsupportedConcreteHandler = false;
var registrations = new List<ImplementationRegistrationSpec>();
// 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<string, HandlerCandidateAnalysis>(StringComparer.Ordinal);
foreach (var candidate in candidates)
@ -141,18 +146,16 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
if (candidate is null)
continue;
if (candidate.Value.HasUnsupportedConcreteHandler)
{
hasUnsupportedConcreteHandler = true;
return [];
}
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) =>
@ -161,9 +164,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
left.ImplementationLogName,
right.ImplementationLogName);
return implementationComparison != 0
? implementationComparison
: StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName);
return implementationComparison;
});
return registrations;
@ -260,6 +261,34 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return builder.ToString();
}
private static string GetReflectionTypeMetadataName(INamedTypeSymbol type)
{
var nestedTypes = new Stack<string>();
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);
@ -270,8 +299,11 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return GetTypeSortKey(type).Replace("global::", string.Empty);
}
private static string GenerateSource(IReadOnlyList<HandlerRegistrationSpec> registrations)
private static string GenerateSource(
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
var hasReflectionRegistrations = registrations.Any(static registration =>
!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName));
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
@ -283,6 +315,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
builder.Append('.');
builder.Append(GeneratedTypeName);
builder.AppendLine("))]");
builder.AppendLine();
builder.Append("namespace ");
builder.Append(GeneratedNamespace);
@ -303,31 +336,179 @@ 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 AppendReflectionRegistration(StringBuilder builder, string reflectionTypeMetadataName)
{
builder.Append(" RegisterReflectedHandler(services, logger, registryAssembly, \"");
builder.Append(EscapeStringLiteral(reflectionTypeMetadataName));
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)
{
return value.Replace("\\", "\\\\")
@ -342,29 +523,41 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
string HandlerInterfaceLogName,
string ImplementationLogName);
private readonly record struct ImplementationRegistrationSpec(
string ImplementationTypeDisplayName,
string ImplementationLogName,
ImmutableArray<HandlerRegistrationSpec> DirectRegistrations,
string? ReflectionTypeMetadataName);
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
{
public HandlerCandidateAnalysis(
string implementationTypeDisplayName,
string implementationLogName,
ImmutableArray<HandlerRegistrationSpec> registrations,
bool hasUnsupportedConcreteHandler)
string? reflectionTypeMetadataName)
{
ImplementationTypeDisplayName = implementationTypeDisplayName;
ImplementationLogName = implementationLogName;
Registrations = registrations;
HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler;
ReflectionTypeMetadataName = reflectionTypeMetadataName;
}
public string ImplementationTypeDisplayName { get; }
public string ImplementationLogName { get; }
public ImmutableArray<HandlerRegistrationSpec> Registrations { get; }
public bool HasUnsupportedConcreteHandler { get; }
public string? ReflectionTypeMetadataName { get; }
public bool Equals(HandlerCandidateAnalysis other)
{
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
StringComparison.Ordinal) ||
HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler ||
!string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) ||
!string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName,
StringComparison.Ordinal) ||
Registrations.Length != other.Registrations.Length)
{
return false;
@ -389,7 +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) ^
(ReflectionTypeMetadataName is null
? 0
: StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName));
foreach (var registration in Registrations)
{
hashCode = (hashCode * 397) ^ registration.GetHashCode();
@ -399,4 +596,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
}
}
}
private readonly record struct GenerationEnvironment(bool GenerationEnabled);
}

View File

@ -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<ICqrsRuntime>(runtime);
container.Register(runtime);
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime);
}
else if (container.Get<LegacyICqrsRuntime>() is null)
{
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)container.GetRequired<ICqrsRuntime>());
}
if (container.Get<ICqrsHandlerRegistrar>() is null)
@ -69,6 +74,14 @@ public static class CqrsTestRuntime
var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
container.Register<ICqrsHandlerRegistrar>(registrar);
}
if (container.Get<ICqrsRegistrationService>() is null)
{
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
var registrar = container.GetRequired<ICqrsHandlerRegistrar>();
var registrationService = CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger);
container.Register<ICqrsRegistrationService>(registrationService);
}
}
/// <summary>