From be59dc7f27ea1b78294cfb0a0b9b7192ad2d22e4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:21:56 +0800 Subject: [PATCH 1/7] =?UTF-8?q?refactor(cqrs):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E5=99=A8=E7=BC=93=E5=AD=98=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将通知、请求和流式处理的缓存从多个独立字典合并为统一的分发绑定结构 - 减少热路径上的重复字典查询操作,提升分发性能 - 重构请求分发绑定缓存,按响应类型分层避免值类型装箱开销 - 添加完整的调度绑定单元测试验证缓存行为正确性 - 简化通知和流式处理的缓存查找逻辑,统一调用模式 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 89 +++++------- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 130 +++++++++--------- 2 files changed, 97 insertions(+), 122 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index badd7490..b16995e4 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -7,14 +7,11 @@ using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Cqrs.Tests.Cqrs; /// -/// 验证 CQRS dispatcher 会缓存热路径中的服务类型与调用委托。 +/// 验证 CQRS dispatcher 会缓存热路径中的 dispatch binding。 /// [TestFixture] internal sealed class CqrsDispatcherCacheTests { - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 初始化测试上下文。 /// @@ -45,40 +42,31 @@ internal sealed class CqrsDispatcherCacheTests _container = null; } + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// - /// 验证相同消息类型重复分发时,不会重复扩张服务类型与调用委托缓存。 + /// 验证相同消息类型重复分发时,不会重复扩张 dispatch binding 缓存。 /// [Test] - public async Task Dispatcher_Should_Cache_Service_Types_After_First_Dispatch() + public async Task Dispatcher_Should_Cache_Dispatch_Bindings_After_First_Dispatch() { - var notificationServiceTypes = GetCacheField("NotificationHandlerServiceTypes"); - var requestServiceTypes = GetCacheField("RequestServiceTypes"); - var streamServiceTypes = GetCacheField("StreamHandlerServiceTypes"); - var requestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers"); - var requestPipelineInvokers = GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers"); - var notificationInvokers = GetCacheField("NotificationInvokers"); - var streamInvokers = GetCacheField("StreamInvokers"); + var notificationBindings = GetCacheField("NotificationDispatchBindings"); + var requestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings"); + var streamBindings = GetCacheField("StreamDispatchBindings"); - 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; + var notificationBefore = notificationBindings.Count; + var requestBefore = requestBindings.Count; + var streamBefore = streamBindings.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; + var notificationAfterFirstDispatch = notificationBindings.Count; + var requestAfterFirstDispatch = requestBindings.Count; + var streamAfterFirstDispatch = streamBindings.Count; await _context.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); @@ -90,38 +78,30 @@ internal sealed class CqrsDispatcherCacheTests 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)); + Assert.That(notificationBindings.Count, Is.EqualTo(notificationAfterFirstDispatch)); + Assert.That(requestBindings.Count, Is.EqualTo(requestAfterFirstDispatch)); + Assert.That(streamBindings.Count, Is.EqualTo(streamAfterFirstDispatch)); }); } /// - /// 验证 request 调用委托会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。 + /// 验证 request dispatch binding 会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。 /// [Test] - public async Task Dispatcher_Should_Cache_Request_Invokers_Per_Response_Type() + public async Task Dispatcher_Should_Cache_Request_Dispatch_Bindings_Per_Response_Type() { - var intRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers"); - var stringRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers"); + var intRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings"); + var stringRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings"); - var intBefore = intRequestInvokers.Count; - var stringBefore = stringRequestInvokers.Count; + var intBefore = intRequestBindings.Count; + var stringBefore = stringRequestBindings.Count; await _context!.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherStringCacheRequest()); - var intAfterFirstDispatch = intRequestInvokers.Count; - var stringAfterFirstDispatch = stringRequestInvokers.Count; + var intAfterFirstDispatch = intRequestBindings.Count; + var stringAfterFirstDispatch = stringRequestBindings.Count; await _context.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherStringCacheRequest()); @@ -130,8 +110,8 @@ internal sealed class CqrsDispatcherCacheTests { Assert.That(intAfterFirstDispatch, Is.EqualTo(intBefore + 1)); Assert.That(stringAfterFirstDispatch, Is.EqualTo(stringBefore + 1)); - Assert.That(intRequestInvokers.Count, Is.EqualTo(intAfterFirstDispatch)); - Assert.That(stringRequestInvokers.Count, Is.EqualTo(stringAfterFirstDispatch)); + Assert.That(intRequestBindings.Count, Is.EqualTo(intAfterFirstDispatch)); + Assert.That(stringRequestBindings.Count, Is.EqualTo(stringAfterFirstDispatch)); }); } @@ -157,15 +137,10 @@ internal sealed class CqrsDispatcherCacheTests /// private static void ClearDispatcherCaches() { - GetCacheField("NotificationHandlerServiceTypes").Clear(); - GetCacheField("RequestServiceTypes").Clear(); - GetCacheField("StreamHandlerServiceTypes").Clear(); - GetCacheField("NotificationInvokers").Clear(); - GetCacheField("StreamInvokers").Clear(); - GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers").Clear(); - GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers").Clear(); - GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers").Clear(); - GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(string), "Invokers").Clear(); + GetCacheField("NotificationDispatchBindings").Clear(); + GetCacheField("StreamDispatchBindings").Clear(); + GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings").Clear(); + GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings").Clear(); } /// diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index a6d62f96..e396998c 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -15,24 +15,13 @@ internal sealed class CqrsDispatcher( IIocContainer container, ILogger logger) : ICqrsRuntime { - // 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。 - private static readonly ConcurrentDictionary NotificationInvokers = new(); + // 进程级缓存:把通知服务类型与调用委托绑定到同一项,减少发布热路径上的重复字典查询。 + private static readonly ConcurrentDictionary + NotificationDispatchBindings = new(); - // 进程级缓存:缓存通知处理器服务类型,避免每次发布都重复 MakeGenericType。 - private static readonly ConcurrentDictionary NotificationHandlerServiceTypes = new(); - - // 进程级缓存:缓存流式请求调用委托,避免每次创建流时重复解析反射签名。 - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = - new(); - - // 进程级缓存:缓存请求处理器与 pipeline 行为的服务类型,减少热路径中的泛型类型构造。 - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestServiceTypeSet> - RequestServiceTypes = new(); - - // 进程级缓存:缓存流式请求处理器服务类型,避免每次建流时重复 MakeGenericType。 - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), Type> - StreamHandlerServiceTypes = - new(); + // 进程级缓存:把流式处理器服务类型与调用委托绑定到同一项,减少建流热路径上的重复字典查询。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamDispatchBinding> + StreamDispatchBindings = new(); // 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。 private static readonly MethodInfo RequestHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) @@ -64,10 +53,10 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(notification); var notificationType = notification.GetType(); - var handlerType = NotificationHandlerServiceTypes.GetOrAdd( + var dispatchBinding = NotificationDispatchBindings.GetOrAdd( notificationType, - static type => typeof(INotificationHandler<>).MakeGenericType(type)); - var handlers = container.GetAll(handlerType); + CreateNotificationDispatchBinding); + var handlers = container.GetAll(dispatchBinding.HandlerType); if (handlers.Count == 0) { @@ -75,14 +64,10 @@ internal sealed class CqrsDispatcher( return; } - var invoker = NotificationInvokers.GetOrAdd( - notificationType, - CreateNotificationInvoker); - foreach (var handler in handlers) { PrepareHandler(handler, context); - await invoker(handler, notification, cancellationToken); + await dispatchBinding.Invoker(handler, notification, cancellationToken); } } @@ -103,36 +88,23 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); - 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) + var dispatchBinding = RequestDispatchBindingCache.Bindings.GetOrAdd( + requestType, + CreateRequestDispatchBinding); + var handler = container.Get(dispatchBinding.HandlerType) ?? throw new InvalidOperationException( $"No CQRS request handler registered for {requestType.FullName}."); PrepareHandler(handler, context); - var behaviors = container.GetAll(serviceTypes.BehaviorType); + var behaviors = container.GetAll(dispatchBinding.BehaviorType); foreach (var behavior in behaviors) PrepareHandler(behavior, context); if (behaviors.Count == 0) - { - var invoker = RequestInvokerCache.Invokers.GetOrAdd( - requestType, - CreateRequestInvoker); + return await dispatchBinding.RequestInvoker(handler, request, cancellationToken); - return await invoker(handler, request, cancellationToken); - } - - var pipelineInvoker = RequestPipelineInvokerCache.Invokers.GetOrAdd( - requestType, - CreateRequestPipelineInvoker); - - return await pipelineInvoker(handler, behaviors, request, cancellationToken); + return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken); } /// @@ -152,20 +124,16 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); - var handlerType = StreamHandlerServiceTypes.GetOrAdd( + var dispatchBinding = StreamDispatchBindings.GetOrAdd( (requestType, typeof(TResponse)), - static key => typeof(IStreamRequestHandler<,>).MakeGenericType(key.RequestType, key.ResponseType)); - var handler = container.Get(handlerType) + static key => CreateStreamDispatchBinding(key.RequestType, key.ResponseType)); + var handler = container.Get(dispatchBinding.HandlerType) ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); PrepareHandler(handler, context); - var invoker = StreamInvokers.GetOrAdd( - (requestType, typeof(TResponse)), - static key => CreateStreamInvoker(key.RequestType, key.ResponseType)); - - return (IAsyncEnumerable)invoker(handler, request, cancellationToken); + return (IAsyncEnumerable)dispatchBinding.Invoker(handler, request, cancellationToken); } /// @@ -185,6 +153,38 @@ internal sealed class CqrsDispatcher( } } + /// + /// 为指定请求类型构造完整分发绑定,把服务类型与强类型调用委托一次性收敛到同一缓存项。 + /// + private static RequestDispatchBinding CreateRequestDispatchBinding(Type requestType) + { + return new RequestDispatchBinding( + typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)), + typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)), + CreateRequestInvoker(requestType), + CreateRequestPipelineInvoker(requestType)); + } + + /// + /// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 + /// + private static NotificationDispatchBinding CreateNotificationDispatchBinding(Type notificationType) + { + return new NotificationDispatchBinding( + typeof(INotificationHandler<>).MakeGenericType(notificationType), + CreateNotificationInvoker(notificationType)); + } + + /// + /// 为指定流式请求类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 + /// + private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType) + { + return new StreamDispatchBinding( + typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType), + CreateStreamInvoker(requestType, responseType)); + } + /// /// 生成请求处理器调用委托,避免每次发送都重复反射。 /// @@ -312,22 +312,22 @@ internal sealed class CqrsDispatcher( private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); /// - /// 按响应类型分层缓存 request 处理器调用委托,避免 value-type 响应在 object 桥接中产生装箱。 + /// 按响应类型分层缓存 request 分发绑定,既避免 value-type 响应走 object 桥接, + /// 也让 handler/pipeline 服务类型与调用委托在热路径上只命中一次缓存。 /// /// 请求响应类型。 - private static class RequestInvokerCache + private static class RequestDispatchBindingCache { - internal static readonly ConcurrentDictionary> Invokers = new(); + internal static readonly ConcurrentDictionary> Bindings = new(); } - /// - /// 按响应类型分层缓存带 pipeline 的 request 调用委托,避免 pipeline 热路径上的额外装箱。 - /// - /// 请求响应类型。 - private static class RequestPipelineInvokerCache - { - internal static readonly ConcurrentDictionary> Invokers = new(); - } + private readonly record struct NotificationDispatchBinding(Type HandlerType, NotificationInvoker Invoker); - private readonly record struct RequestServiceTypeSet(Type HandlerType, Type BehaviorType); + private readonly record struct StreamDispatchBinding(Type HandlerType, StreamInvoker Invoker); + + private readonly record struct RequestDispatchBinding( + Type HandlerType, + Type BehaviorType, + RequestInvoker RequestInvoker, + RequestPipelineInvoker PipelineInvoker); } From c7516800e7132d02efe5fbc51b9ca7dd1a48dd57 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:19:35 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E8=87=AA=E5=8A=A8=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 CqrsHandlerRegistrar 类,支持扫描并注册 CQRS 请求/通知/流式处理器 - 添加程序集级源码生成注册器支持,减少冷启动反射开销 - 实现反射回退机制,在生成注册器不可用时进行类型扫描 - 添加进程级缓存机制,避免重复分析程序集元数据和类型加载 - 支持确定性的处理器注册顺序,按名称排序保证稳定性 - 实现类型加载容错机制,部分类型失败时保留其他处理器注册 - 添加完整的单元测试覆盖,验证各种注册场景和错误处理 - 实现日志记录功能,提供详细的注册过程诊断信息 --- .../Cqrs/CqrsHandlerRegistrarTests.cs | 91 +++++++++++++ .../Internal/CqrsHandlerRegistrar.cs | 120 ++++++++++++++++-- 2 files changed, 197 insertions(+), 14 deletions(-) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index b44b0bb1..ea1fae30 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -292,6 +292,97 @@ internal sealed class CqrsHandlerRegistrarTests Times.Never); generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never); } + + /// + /// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据, + /// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。 + /// + [Test] + public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers() + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, 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 firstContainer = new MicrosoftDiContainer(); + var secondContainer = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(firstContainer, generatedAssembly.Object); + CqrsTestRuntime.RegisterHandlers(secondContainer, generatedAssembly.Object); + + generatedAssembly.Verify( + static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false), + Times.Once); + generatedAssembly.Verify( + static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false), + Times.Once); + generatedAssembly.Verify( + static assembly => assembly.GetType( + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!, + false, + false), + Times.Once); + } + + /// + /// 验证同一程序集对象在未命中 generated registry 时,会复用首次扫描得到的可加载类型列表, + /// 而不是为每个容器重复执行整程序集 GetTypes()。 + /// + [Test] + public void RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers() + { + var reflectionTypeLoadException = new ReflectionTypeLoadException( + [typeof(AlphaDeterministicNotificationHandler), null], + [new TypeLoadException("Cached loadable-type probe.")]); + var partiallyLoadableAssembly = new Mock(); + partiallyLoadableAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns("GFramework.Core.Tests.Cqrs.CachedLoadableTypesAssembly, Version=1.0.0.0"); + partiallyLoadableAssembly + .Setup(static assembly => assembly.GetTypes()) + .Throws(reflectionTypeLoadException); + + var firstContainer = new MicrosoftDiContainer(); + var secondContainer = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(firstContainer, partiallyLoadableAssembly.Object); + CqrsTestRuntime.RegisterHandlers(secondContainer, partiallyLoadableAssembly.Object); + firstContainer.Freeze(); + secondContainer.Freeze(); + + Assert.Multiple(() => + { + Assert.That( + firstContainer.GetAll>() + .Select(static handler => handler.GetType()) + .ToArray(), + Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + Assert.That( + secondContainer.GetAll>() + .Select(static handler => handler.GetType()) + .ToArray(), + Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)])); + }); + + partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); + } } /// diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 3604de83..a4f20396 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -11,6 +11,19 @@ namespace GFramework.Cqrs.Internal; /// internal static class CqrsHandlerRegistrar { + // 进程级缓存:同一程序集的 generated-registry 元数据与 reflection-fallback 元数据在加载后保持稳定, + // 因此可跨容器复用分析结果,避免每次注册都重复读取程序集级 attribute。 + private static readonly ConcurrentDictionary AssemblyMetadataCache = + new(ReferenceEqualityComparer.Instance); + + // 进程级缓存:registry 类型的可激活性与构造入口是稳定的,可跨多次容器初始化复用。 + private static readonly ConcurrentDictionary RegistryActivationMetadataCache = + new(); + + // 进程级缓存:对未命中 generated-registry 的程序集,缓存可加载类型列表以避免重复 GetTypes() 扫描。 + private static readonly ConcurrentDictionary> LoadableTypesCache = + new(ReferenceEqualityComparer.Instance); + /// /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 /// @@ -60,14 +73,10 @@ internal static class CqrsHandlerRegistrar try { - var registryTypes = assembly - .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) - .OfType() - .Select(static attribute => attribute.RegistryType) - .Where(static type => type is not null) - .Distinct() - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); + var assemblyMetadata = AssemblyMetadataCache.GetOrAdd( + assembly, + key => AnalyzeAssemblyRegistrationMetadata(key, logger)); + var registryTypes = assemblyMetadata.RegistryTypes; if (registryTypes.Count == 0) return GeneratedRegistrationResult.NoGeneratedRegistry(); @@ -75,27 +84,32 @@ internal static class CqrsHandlerRegistrar var registries = new List(registryTypes.Count); foreach (var registryType in registryTypes) { - if (!typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType)) + var activationMetadata = RegistryActivationMetadataCache.GetOrAdd( + registryType, + AnalyzeRegistryActivation); + + if (!activationMetadata.ImplementsRegistryContract) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); return GeneratedRegistrationResult.NoGeneratedRegistry(); } - if (registryType.IsAbstract) + if (activationMetadata.IsAbstract) { logger.Warn( $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); return GeneratedRegistrationResult.NoGeneratedRegistry(); } - if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry) + if (activationMetadata.Factory is null) { logger.Warn( - $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated."); + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor."); return GeneratedRegistrationResult.NoGeneratedRegistry(); } + var registry = activationMetadata.Factory(); registries.Add(registry); } @@ -106,7 +120,7 @@ internal static class CqrsHandlerRegistrar registry.Register(services, logger); } - var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger); + var reflectionFallbackMetadata = assemblyMetadata.ReflectionFallbackMetadata; if (reflectionFallbackMetadata is not null) { if (reflectionFallbackMetadata.HasExplicitTypes) @@ -259,13 +273,69 @@ internal static class CqrsHandlerRegistrar /// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。 /// private static IReadOnlyList GetLoadableTypes(Assembly assembly, ILogger logger) + { + return LoadableTypesCache.GetOrAdd( + assembly, + key => LoadAndSortTypes(key, logger)); + } + + /// + /// 分析并缓存指定程序集上的 generated-registry 与 fallback 元数据。 + /// + private static AssemblyRegistrationMetadata AnalyzeAssemblyRegistrationMetadata( + Assembly assembly, + ILogger logger) + { + var registryTypes = assembly + .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) + .OfType() + .Select(static attribute => attribute.RegistryType) + .Where(static type => type is not null) + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray(); + + var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger); + return new AssemblyRegistrationMetadata(registryTypes, reflectionFallbackMetadata); + } + + /// + /// 分析并缓存 registry 类型的可激活性,避免每次注册都重复检查接口实现与构造函数。 + /// + private static RegistryActivationMetadata AnalyzeRegistryActivation(Type registryType) + { + var implementsRegistryContract = typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType); + if (!implementsRegistryContract) + return new RegistryActivationMetadata(false, registryType.IsAbstract, null); + + if (registryType.IsAbstract) + return new RegistryActivationMetadata(true, true, null); + + var constructor = registryType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + Type.EmptyTypes, + modifiers: null); + + return constructor is null + ? new RegistryActivationMetadata(true, false, null) + : new RegistryActivationMetadata( + true, + false, + () => (ICqrsHandlerRegistry)constructor.Invoke(null)); + } + + /// + /// 首次命中未生成 registry 的程序集时加载并排序全部可扫描类型,后续复用缓存结果。 + /// + private static IReadOnlyList LoadAndSortTypes(Assembly assembly, ILogger logger) { try { return assembly.GetTypes() .Where(static type => type is not null) .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToList(); + .ToArray(); } catch (ReflectionTypeLoadException exception) { @@ -390,4 +460,26 @@ internal static class CqrsHandlerRegistrar public bool HasExplicitTypes => Types.Count > 0; } + + private sealed class AssemblyRegistrationMetadata( + IReadOnlyList registryTypes, + ReflectionFallbackMetadata? reflectionFallbackMetadata) + { + public IReadOnlyList RegistryTypes { get; } = + registryTypes ?? throw new ArgumentNullException(nameof(registryTypes)); + + public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata; + } + + private sealed class RegistryActivationMetadata( + bool implementsRegistryContract, + bool isAbstract, + Func? factory) + { + public bool ImplementsRegistryContract { get; } = implementsRegistryContract; + + public bool IsAbstract { get; } = isAbstract; + + public Func? Factory { get; } = factory; + } } From 38f98ea7ea360134d411c3028a2a1d725d2eb4ce Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:33:29 +0800 Subject: [PATCH 3/7] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E6=A8=A1=E5=BC=8F=E5=92=8C=E6=BA=90=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8=E5=AE=8C=E6=95=B4=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 核心概念、命令查询处理器实现指南 - 添加请求分发器、通知发布和管道行为使用说明 - 提供流式处理、验证行为等高级特性文档 - 完善源代码生成器模块介绍和配置说明 - 包含 Log、ContextAware、EnumExtensions 等生成器详细用法 - 添加 Godot 专用生成器如 GetNode、AutoUiPage 等使用指南 - 整理诊断信息、最佳实践和常见问题解答 --- CLAUDE.md | 13 +++++++++--- README.md | 30 +++++++++++++++++---------- docs/zh-CN/core/cqrs.md | 22 ++++++++++---------- docs/zh-CN/source-generators/index.md | 8 +++---- 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b847b5cd..9878a18e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,17 +18,22 @@ Follow them strictly. ```text GFramework (meta package) ─→ Core + Game +GFramework.Cqrs ─→ Cqrs.Abstractions, Core.Abstractions GFramework.Core ─→ Core.Abstractions GFramework.Game ─→ Game.Abstractions, Core, Core.Abstractions GFramework.Godot ─→ Core, Game, Core.Abstractions, Game.Abstractions GFramework.Ecs.Arch ─→ Ecs.Arch.Abstractions, Core, Core.Abstractions -GFramework.SourceGenerators ─→ SourceGenerators.Common, SourceGenerators.Abstractions +GFramework.Core.SourceGenerators ─→ Core.SourceGenerators.Abstractions, SourceGenerators.Common +GFramework.Game.SourceGenerators ─→ SourceGenerators.Common +GFramework.Godot.SourceGenerators ─→ Godot.SourceGenerators.Abstractions, SourceGenerators.Common +GFramework.Cqrs.SourceGenerators ─→ SourceGenerators.Common ``` - **Abstractions projects** (`netstandard2.1`): 只包含接口和契约定义,不承载运行时实现逻辑。 - **Core / Game / Ecs.Arch** (`net8.0;net9.0;net10.0`): 平台无关的核心实现层。 - **Godot**: Godot 引擎集成层,负责与节点、场景和引擎生命周期对接。 -- **SourceGenerators** (`netstandard2.1`): Roslyn 增量源码生成器及其公共基础设施。 +- **SourceGenerators family** (`netstandard2.0`/`netstandard2.1`): 按 Core / Game / Godot / Cqrs 拆分的 Roslyn + 增量源码生成器,以及共享的 abstractions/common 基础设施。 ## Architecture Pattern @@ -114,10 +119,12 @@ Architecture 负责统一生命周期编排,核心阶段包括: 仓库以“抽象层 + 实现层 + 集成层 + 生成器层”的方式组织: - `GFramework.Core.Abstractions` / `GFramework.Game.Abstractions`: 约束接口和公共契约。 +- `GFramework.Cqrs.Abstractions` / `GFramework.Cqrs`: 提供 CQRS 契约、runtime 与 handler 注册基础设施。 - `GFramework.Core` / `GFramework.Game`: 提供平台无关实现。 - `GFramework.Godot`: 提供与 Godot 运行时集成的适配实现。 - `GFramework.Ecs.Arch`: 提供 ECS Architecture 相关扩展。 -- `GFramework.SourceGenerators` 及相关 Abstractions/Common: 提供代码生成能力。 +- `GFramework.Core.SourceGenerators` / `GFramework.Game.SourceGenerators` / `GFramework.Godot.SourceGenerators` / + `GFramework.Cqrs.SourceGenerators` 与相关 Abstractions/Common: 提供代码生成能力。 这种结构的核心设计目标是让抽象稳定、实现可替换、引擎集成隔离、生成器能力可独立演进。 diff --git a/README.md b/README.md index 4fe20e29..0f8b9a1c 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,17 @@ GFramework 采用清晰分层与模块化设计,强调: ## 功能模块 -| 模块 | 说明 | 文档 | -| --- | --- | --- | -| `GFramework.Core` | 平台无关的核心架构能力(架构、命令、查询、事件、属性、IOC、日志等) | [查看](GFramework.Core/README.md) | -| `GFramework.Core.Abstractions` | Core 对应的抽象接口定义 | [查看](GFramework.Core.Abstractions/README.md) | -| `GFramework.Game` | 游戏业务侧扩展(状态、配置、存储、UI 等) | [查看](GFramework.Game/README.md) | -| `GFramework.Game.Abstractions` | Game 模块抽象接口定义 | [查看](GFramework.Game.Abstractions/README.md) | -| `GFramework.Godot` | Godot 集成层(节点扩展、场景/设置/存储适配等) | [查看](GFramework.Godot/README.md) | -| `GFramework.SourceGenerators` | 通用源码生成器(日志、枚举扩展、规则等) | [查看](GFramework.SourceGenerators/README.md) | -| `GFramework.Godot.SourceGenerators` | Godot 场景下的源码生成器扩展 | [查看](GFramework.Godot.SourceGenerators/README.md) | +| 模块 | 说明 | 文档 | +|-------------------------------------------------------------------------|-----------------------------------------|---------------------------------------------------| +| `GFramework.Core` | 平台无关的核心架构能力(架构、命令、查询、事件、属性、IOC、日志等) | [查看](GFramework.Core/README.md) | +| `GFramework.Core.Abstractions` | Core 对应的抽象接口定义 | [查看](GFramework.Core.Abstractions/README.md) | +| `GFramework.Game` | 游戏业务侧扩展(状态、配置、存储、UI 等) | [查看](GFramework.Game/README.md) | +| `GFramework.Game.Abstractions` | Game 模块抽象接口定义 | [查看](GFramework.Game.Abstractions/README.md) | +| `GFramework.Godot` | Godot 集成层(节点扩展、场景/设置/存储适配等) | [查看](GFramework.Godot/README.md) | +| `GFramework.Cqrs` / `GFramework.Cqrs.Abstractions` | CQRS runtime、契约与 handler 注册基础设施 | [查看](docs/zh-CN/core/cqrs.md) | +| `GFramework.Core.SourceGenerators` | Core 侧源码生成器(日志、枚举扩展、规则、模块注册等) | [查看](GFramework.Core.SourceGenerators/README.md) | +| `GFramework.Game.SourceGenerators` / `GFramework.Cqrs.SourceGenerators` | 游戏配置 schema 与 CQRS handler registry 生成器 | [查看](docs/zh-CN/source-generators/index.md) | +| `GFramework.Godot.SourceGenerators` | Godot 场景下的源码生成器扩展 | [查看](GFramework.Godot.SourceGenerators/README.md) | ## 文档导航 @@ -50,7 +52,7 @@ GFramework 采用清晰分层与模块化设计,强调: - **`GeWuYou.GFramework`**:聚合元包(Meta Package),用于一键引入常用能力集合,适合快速试用或原型阶段。 - **`GeWuYou.GFramework.Core`**:核心起步包,适合希望按模块精细控制依赖的项目(推荐生产项目从此起步)。 -如果你已明确技术栈,建议优先按模块安装(Core / Game / Godot / SourceGenerators),避免不必要依赖。 +如果你已明确技术栈,建议优先按模块安装(Core / Cqrs / Game / Godot / Source Generators),避免不必要依赖。 ## 快速安装 @@ -105,13 +107,19 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators ```text GFramework.sln +├─ GFramework.Cqrs/ +├─ GFramework.Cqrs.Abstractions/ ├─ GFramework.Core/ ├─ GFramework.Core.Abstractions/ +├─ GFramework.Core.SourceGenerators/ +├─ GFramework.Core.SourceGenerators.Abstractions/ ├─ GFramework.Game/ ├─ GFramework.Game.Abstractions/ +├─ GFramework.Game.SourceGenerators/ ├─ GFramework.Godot/ -├─ GFramework.SourceGenerators/ ├─ GFramework.Godot.SourceGenerators/ +├─ GFramework.Cqrs.SourceGenerators/ +├─ GFramework.SourceGenerators.Common/ ├─ docs/ └─ docfx/ ``` diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index c0357fa5..9f6621f4 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -28,8 +28,8 @@ CQRS(Command Query Responsibility Segregation,命令查询职责分离)是 命令表示修改系统状态的操作,如创建、更新、删除: ```csharp -using GFramework.Core.CQRS.Command; -using GFramework.Core.Abstractions.CQRS.Command; +using GFramework.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; // 定义命令输入 public class CreatePlayerInput : ICommandInput @@ -50,8 +50,8 @@ public class CreatePlayerCommand : CommandBase 查询表示读取系统状态的操作,不修改数据: ```csharp -using GFramework.Core.CQRS.Query; -using GFramework.Core.Abstractions.CQRS.Query; +using GFramework.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; // 定义查询输入 public class GetPlayerInput : IQueryInput @@ -71,7 +71,7 @@ public class GetPlayerQuery : QueryBase 处理器负责执行命令或查询的具体逻辑: ```csharp -using GFramework.Core.CQRS.Command; +using GFramework.Cqrs.Cqrs.Command; // 命令处理器 public class CreatePlayerCommandHandler : AbstractCommandHandler @@ -247,8 +247,8 @@ public class GameArchitecture : Architecture handler。 `RegisterCqrsPipelineBehavior()` 是唯一保留的公开入口;旧的 `Mediator` 兼容别名与扩展已移除,不再继续维护。 -如果你正在从旧版本迁移,显式替换关系就是 -`RegisterMediatorBehavior() -> RegisterCqrsPipelineBehavior()`。 +如果你正在从旧版本迁移,只需要直接改用 `RegisterCqrsPipelineBehavior()`; +旧 `RegisterMediatorBehavior()` 已移除,不再保留兼容入口。 当前接口支持两种形式: - 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 @@ -261,8 +261,8 @@ handler。 Request 是更通用的消息类型,可以用于任何场景: ```csharp -using GFramework.Core.CQRS.Request; -using GFramework.Core.Abstractions.CQRS.Request; +using GFramework.Cqrs.Request; +using GFramework.Cqrs.Abstractions.Cqrs.Request; // 定义请求输入 public class ValidatePlayerInput : IRequestInput @@ -299,8 +299,8 @@ public class ValidatePlayerRequestHandler : AbstractRequestHandler 编译时代码生成 - 零运行时开销的代码增强工具 -GFramework.SourceGenerators 是 GFramework 框架的源代码生成器包,通过编译时分析自动生成样板代码,显著提升开发效率并减少运行时开销。 +GFramework 当前按模块提供一组 Source Generators,通过编译时分析自动生成样板代码,显著提升开发效率并减少运行时开销。 ## 📋 目录 @@ -30,7 +30,7 @@ GFramework.SourceGenerators 是 GFramework 框架的源代码生成器包,通 ## 概述 -GFramework 的 source generators 利用 Roslyn 源代码生成器技术,在编译时分析你的代码并自动生成常用的样板代码,让开发者专注于业务逻辑而不是重复的模板代码。 +GFramework 的 Source Generators 利用 Roslyn 源代码生成器技术,在编译时分析你的代码并自动生成常用的样板代码,让开发者专注于业务逻辑而不是重复的模板代码。 当前 NuGet 发布按模块拆分为: @@ -693,7 +693,7 @@ public partial class GameEntryPoint : Node ## 诊断信息 -GFramework.SourceGenerators 提供详细的编译时诊断信息,帮助开发者快速定位和解决问题。 +这一组 Source Generators 提供详细的编译时诊断信息,帮助开发者快速定位和解决问题。 ### GF_Logging_001 - 日志字段名冲突 From 04123d2a716dd9ebca5af6e193d9e5e79914aea6 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:41:38 +0800 Subject: [PATCH 4/7] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E6=A8=A1=E5=BC=8F=E4=B8=8E=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=E5=AE=8C=E6=95=B4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CQRS 核心概念、命令查询处理器实现指南 - 添加安装配置文档,包含各模块包说明与环境要求 - 实现源码生成器全面文档,涵盖 Log、Config Schema、CQRS Handler Registry 等特性 - 提供详细用法示例与最佳实践指导 - 包含常见问题解答与故障排查方案 - 添加 Godot 项目集成与性能优化相关内容 --- .../Cqrs/CqrsDispatcherCacheTests.cs | 145 +++++++++----- .../Cqrs/CqrsHandlerRegistrarTests.cs | 27 +++ GFramework.Cqrs/Internal/CqrsDispatcher.cs | 127 ++++++++++--- .../Internal/CqrsHandlerRegistrar.cs | 21 ++- GFramework.Cqrs/Internal/WeakKeyCache.cs | 178 ++++++++++++++++++ README.md | 4 + docs/zh-CN/core/cqrs.md | 15 ++ docs/zh-CN/getting-started/installation.md | 40 ++-- docs/zh-CN/source-generators/index.md | 66 +++++++ 9 files changed, 528 insertions(+), 95 deletions(-) create mode 100644 GFramework.Cqrs/Internal/WeakKeyCache.cs diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index b16995e4..470667ec 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -12,6 +12,9 @@ namespace GFramework.Cqrs.Tests.Cqrs; [TestFixture] internal sealed class CqrsDispatcherCacheTests { + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// /// 初始化测试上下文。 /// @@ -42,9 +45,6 @@ internal sealed class CqrsDispatcherCacheTests _container = null; } - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 验证相同消息类型重复分发时,不会重复扩张 dispatch binding 缓存。 /// @@ -52,21 +52,38 @@ internal sealed class CqrsDispatcherCacheTests public async Task Dispatcher_Should_Cache_Dispatch_Bindings_After_First_Dispatch() { var notificationBindings = GetCacheField("NotificationDispatchBindings"); - var requestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings"); + var requestBindings = GetCacheField("RequestDispatchBindings"); var streamBindings = GetCacheField("StreamDispatchBindings"); - var notificationBefore = notificationBindings.Count; - var requestBefore = requestBindings.Count; - var streamBefore = streamBindings.Count; + Assert.Multiple(() => + { + Assert.That( + GetSingleKeyCacheValue(notificationBindings, typeof(DispatcherCacheNotification)), + Is.Null); + Assert.That( + GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int)), + Is.Null); + Assert.That( + GetPairCacheValue(requestBindings, typeof(DispatcherPipelineCacheRequest), typeof(int)), + Is.Null); + Assert.That( + GetPairCacheValue(streamBindings, typeof(DispatcherCacheStreamRequest), typeof(int)), + Is.Null); + }); await _context!.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); await _context.PublishAsync(new DispatcherCacheNotification()); await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest())); - var notificationAfterFirstDispatch = notificationBindings.Count; - var requestAfterFirstDispatch = requestBindings.Count; - var streamAfterFirstDispatch = streamBindings.Count; + var notificationAfterFirstDispatch = + GetSingleKeyCacheValue(notificationBindings, typeof(DispatcherCacheNotification)); + var requestAfterFirstDispatch = + GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int)); + var pipelineAfterFirstDispatch = + GetPairCacheValue(requestBindings, typeof(DispatcherPipelineCacheRequest), typeof(int)); + var streamAfterFirstDispatch = + GetPairCacheValue(streamBindings, typeof(DispatcherCacheStreamRequest), typeof(int)); await _context.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); @@ -75,13 +92,23 @@ internal sealed class CqrsDispatcherCacheTests 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(notificationAfterFirstDispatch, Is.Not.Null); + Assert.That(requestAfterFirstDispatch, Is.Not.Null); + Assert.That(pipelineAfterFirstDispatch, Is.Not.Null); + Assert.That(streamAfterFirstDispatch, Is.Not.Null); - Assert.That(notificationBindings.Count, Is.EqualTo(notificationAfterFirstDispatch)); - Assert.That(requestBindings.Count, Is.EqualTo(requestAfterFirstDispatch)); - Assert.That(streamBindings.Count, Is.EqualTo(streamAfterFirstDispatch)); + Assert.That( + GetSingleKeyCacheValue(notificationBindings, typeof(DispatcherCacheNotification)), + Is.SameAs(notificationAfterFirstDispatch)); + Assert.That( + GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int)), + Is.SameAs(requestAfterFirstDispatch)); + Assert.That( + GetPairCacheValue(requestBindings, typeof(DispatcherPipelineCacheRequest), typeof(int)), + Is.SameAs(pipelineAfterFirstDispatch)); + Assert.That( + GetPairCacheValue(streamBindings, typeof(DispatcherCacheStreamRequest), typeof(int)), + Is.SameAs(streamAfterFirstDispatch)); }); } @@ -91,34 +118,37 @@ internal sealed class CqrsDispatcherCacheTests [Test] public async Task Dispatcher_Should_Cache_Request_Dispatch_Bindings_Per_Response_Type() { - var intRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings"); - var stringRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings"); - - var intBefore = intRequestBindings.Count; - var stringBefore = stringRequestBindings.Count; + var requestBindings = GetCacheField("RequestDispatchBindings"); await _context!.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherStringCacheRequest()); - var intAfterFirstDispatch = intRequestBindings.Count; - var stringAfterFirstDispatch = stringRequestBindings.Count; + var intAfterFirstDispatch = + GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int)); + var stringAfterFirstDispatch = + GetPairCacheValue(requestBindings, typeof(DispatcherStringCacheRequest), typeof(string)); await _context.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherStringCacheRequest()); Assert.Multiple(() => { - Assert.That(intAfterFirstDispatch, Is.EqualTo(intBefore + 1)); - Assert.That(stringAfterFirstDispatch, Is.EqualTo(stringBefore + 1)); - Assert.That(intRequestBindings.Count, Is.EqualTo(intAfterFirstDispatch)); - Assert.That(stringRequestBindings.Count, Is.EqualTo(stringAfterFirstDispatch)); + Assert.That(intAfterFirstDispatch, Is.Not.Null); + Assert.That(stringAfterFirstDispatch, Is.Not.Null); + Assert.That(intAfterFirstDispatch, Is.Not.SameAs(stringAfterFirstDispatch)); + Assert.That( + GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int)), + Is.SameAs(intAfterFirstDispatch)); + Assert.That( + GetPairCacheValue(requestBindings, typeof(DispatcherStringCacheRequest), typeof(string)), + Is.SameAs(stringAfterFirstDispatch)); }); } /// - /// 通过反射读取 dispatcher 的静态缓存字典。 + /// 通过反射读取 dispatcher 的静态缓存对象。 /// - private static IDictionary GetCacheField(string fieldName) + private static object GetCacheField(string fieldName) { var dispatcherType = GetDispatcherType(); var field = dispatcherType.GetField( @@ -127,9 +157,9 @@ internal sealed class CqrsDispatcherCacheTests Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}."); - return field!.GetValue(null) as IDictionary + return field!.GetValue(null) ?? throw new InvalidOperationException( - $"Dispatcher cache field {fieldName} does not implement IDictionary."); + $"Dispatcher cache field {fieldName} returned null."); } /// @@ -137,36 +167,47 @@ internal sealed class CqrsDispatcherCacheTests /// private static void ClearDispatcherCaches() { - GetCacheField("NotificationDispatchBindings").Clear(); - GetCacheField("StreamDispatchBindings").Clear(); - GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings").Clear(); - GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings").Clear(); + ClearCache(GetCacheField("NotificationDispatchBindings")); + ClearCache(GetCacheField("RequestDispatchBindings")); + ClearCache(GetCacheField("StreamDispatchBindings")); } /// - /// 通过反射读取 dispatcher 嵌套泛型缓存类型上的静态缓存字典。 + /// 读取单键缓存中当前保存的对象。 /// - private static IDictionary GetGenericCacheField(string nestedTypeName, Type genericTypeArgument, string fieldName) + private static object? GetSingleKeyCacheValue(object cache, Type key) { - var nestedGenericType = GetDispatcherType().GetNestedType( - nestedTypeName, - BindingFlags.NonPublic); + return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", key); + } - Assert.That(nestedGenericType, Is.Not.Null, $"Missing dispatcher nested cache type {nestedTypeName}."); + /// + /// 读取双键缓存中当前保存的对象。 + /// + private static object? GetPairCacheValue(object cache, Type primaryType, Type secondaryType) + { + return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType); + } - var closedNestedType = nestedGenericType!.MakeGenericType(genericTypeArgument); - var field = closedNestedType.GetField( - fieldName, - BindingFlags.NonPublic | BindingFlags.Static); + /// + /// 调用缓存实例上的无参清理方法。 + /// + private static void ClearCache(object cache) + { + _ = InvokeInstanceMethod(cache, "Clear"); + } - Assert.That( - field, - Is.Not.Null, - $"Missing dispatcher nested cache field {nestedTypeName}.{fieldName} for {genericTypeArgument.FullName}."); + /// + /// 调用缓存对象上的实例方法。 + /// + private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments) + { + var method = target.GetType().GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - return field!.GetValue(null) as IDictionary - ?? throw new InvalidOperationException( - $"Dispatcher nested cache field {nestedTypeName}.{fieldName} does not implement IDictionary."); + Assert.That(method, Is.Not.Null, $"Missing cache method {target.GetType().FullName}.{methodName}."); + + return method!.Invoke(target, arguments); } /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index ea1fae30..3e92281d 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -326,6 +326,33 @@ internal sealed class CqrsHandlerRegistrarTests CqrsTestRuntime.RegisterHandlers(firstContainer, generatedAssembly.Object); CqrsTestRuntime.RegisterHandlers(secondContainer, generatedAssembly.Object); + firstContainer.Freeze(); + secondContainer.Freeze(); + + var firstRegistrations = firstContainer.GetAll>() + .Select(static handler => handler.GetType()) + .ToArray(); + var secondRegistrations = secondContainer.GetAll>() + .Select(static handler => handler.GetType()) + .ToArray(); + + Assert.Multiple(() => + { + Assert.That( + firstRegistrations, + Is.EqualTo( + [ + typeof(GeneratedRegistryNotificationHandler), + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + ])); + Assert.That( + secondRegistrations, + Is.EqualTo( + [ + typeof(GeneratedRegistryNotificationHandler), + ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType + ])); + }); generatedAssembly.Verify( static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false), diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index e396998c..d229d190 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -15,14 +15,20 @@ internal sealed class CqrsDispatcher( IIocContainer container, ILogger logger) : ICqrsRuntime { - // 进程级缓存:把通知服务类型与调用委托绑定到同一项,减少发布热路径上的重复字典查询。 - private static readonly ConcurrentDictionary + // 卸载安全的进程级缓存:通知类型只以弱键语义保留。 + // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。 + private static readonly WeakKeyCache NotificationDispatchBindings = new(); - // 进程级缓存:把流式处理器服务类型与调用委托绑定到同一项,减少建流热路径上的重复字典查询。 - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamDispatchBinding> + // 卸载安全的进程级缓存:请求/响应类型对采用弱键缓存,避免流式消息类型被静态字典永久保留。 + private static readonly WeakTypePairCache StreamDispatchBindings = new(); + // 卸载安全的进程级缓存:请求/响应类型对命中后复用强类型 dispatch binding; + // 若任一类型被回收,后续首次发送时会按当前加载状态重新生成。 + private static readonly WeakTypePairCache + RequestDispatchBindings = new(); + // 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。 private static readonly MethodInfo RequestHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; @@ -88,9 +94,7 @@ internal sealed class CqrsDispatcher( ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); - var dispatchBinding = RequestDispatchBindingCache.Bindings.GetOrAdd( - requestType, - CreateRequestDispatchBinding); + var dispatchBinding = GetRequestDispatchBinding(requestType); var handler = container.Get(dispatchBinding.HandlerType) ?? throw new InvalidOperationException( $"No CQRS request handler registered for {requestType.FullName}."); @@ -125,8 +129,9 @@ internal sealed class CqrsDispatcher( var requestType = request.GetType(); var dispatchBinding = StreamDispatchBindings.GetOrAdd( - (requestType, typeof(TResponse)), - static key => CreateStreamDispatchBinding(key.RequestType, key.ResponseType)); + requestType, + typeof(TResponse), + CreateStreamDispatchBinding); var handler = container.Get(dispatchBinding.HandlerType) ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); @@ -165,6 +170,32 @@ internal sealed class CqrsDispatcher( CreateRequestPipelineInvoker(requestType)); } + /// + /// 获取指定请求/响应类型对的 dispatch binding;若缓存未命中则按当前加载状态创建。 + /// + private static RequestDispatchBinding GetRequestDispatchBinding(Type requestType) + { + var bindingBox = RequestDispatchBindings.GetOrAdd( + requestType, + typeof(TResponse), + CreateRequestDispatchBindingBox); + return bindingBox.Get(); + } + + /// + /// 为弱键请求缓存创建强类型 binding 盒子,避免 value-type 响应走 object 结果桥接。 + /// + private static RequestDispatchBindingBox CreateRequestDispatchBindingBox( + Type requestType, + Type responseType) + { + if (responseType != typeof(TResponse)) + throw new InvalidOperationException( + $"Request dispatch binding cache expected response type {typeof(TResponse).FullName}, but received {responseType.FullName}."); + + return RequestDispatchBindingBox.Create(CreateRequestDispatchBinding(requestType)); + } + /// /// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 /// @@ -312,22 +343,76 @@ internal sealed class CqrsDispatcher( private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); /// - /// 按响应类型分层缓存 request 分发绑定,既避免 value-type 响应走 object 桥接, - /// 也让 handler/pipeline 服务类型与调用委托在热路径上只命中一次缓存。 + /// 将不同响应类型的 request dispatch binding 包装到统一弱缓存值中, + /// 同时保留强类型委托,避免值类型响应退化为 object 桥接。 /// - /// 请求响应类型。 - private static class RequestDispatchBindingCache + private abstract class RequestDispatchBindingBox { - internal static readonly ConcurrentDictionary> Bindings = new(); + /// + /// 创建一个新的强类型 dispatch binding 盒子。 + /// + public static RequestDispatchBindingBox Create(RequestDispatchBinding binding) + { + ArgumentNullException.ThrowIfNull(binding); + return new RequestDispatchBindingBox(binding); + } + + /// + /// 读取指定响应类型的 request dispatch binding。 + /// + public abstract RequestDispatchBinding Get(); } - private readonly record struct NotificationDispatchBinding(Type HandlerType, NotificationInvoker Invoker); + /// + /// 保存特定响应类型的 request dispatch binding。 + /// + /// 请求响应类型。 + private sealed class RequestDispatchBindingBox(RequestDispatchBinding binding) + : RequestDispatchBindingBox + { + private readonly RequestDispatchBinding _binding = binding; - private readonly record struct StreamDispatchBinding(Type HandlerType, StreamInvoker Invoker); + /// + /// 以原始强类型返回当前 binding;若请求的响应类型不匹配则抛出异常。 + /// + public override RequestDispatchBinding Get() + { + if (typeof(TRequestedResponse) != typeof(TResponse)) + { + throw new InvalidOperationException( + $"Cached request dispatch binding for {typeof(TResponse).FullName} cannot be used as {typeof(TRequestedResponse).FullName}."); + } - private readonly record struct RequestDispatchBinding( - Type HandlerType, - Type BehaviorType, - RequestInvoker RequestInvoker, - RequestPipelineInvoker PipelineInvoker); + return (RequestDispatchBinding)(object)_binding; + } + } + + private sealed class NotificationDispatchBinding(Type handlerType, NotificationInvoker invoker) + { + public Type HandlerType { get; } = handlerType; + + public NotificationInvoker Invoker { get; } = invoker; + } + + private sealed class StreamDispatchBinding(Type handlerType, StreamInvoker invoker) + { + public Type HandlerType { get; } = handlerType; + + public StreamInvoker Invoker { get; } = invoker; + } + + private sealed class RequestDispatchBinding( + Type handlerType, + Type behaviorType, + RequestInvoker requestInvoker, + RequestPipelineInvoker pipelineInvoker) + { + public Type HandlerType { get; } = handlerType; + + public Type BehaviorType { get; } = behaviorType; + + public RequestInvoker RequestInvoker { get; } = requestInvoker; + + public RequestPipelineInvoker PipelineInvoker { get; } = pipelineInvoker; + } } diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index a4f20396..031cb7e4 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -11,18 +11,19 @@ namespace GFramework.Cqrs.Internal; /// internal static class CqrsHandlerRegistrar { - // 进程级缓存:同一程序集的 generated-registry 元数据与 reflection-fallback 元数据在加载后保持稳定, - // 因此可跨容器复用分析结果,避免每次注册都重复读取程序集级 attribute。 - private static readonly ConcurrentDictionary AssemblyMetadataCache = - new(ReferenceEqualityComparer.Instance); - - // 进程级缓存:registry 类型的可激活性与构造入口是稳定的,可跨多次容器初始化复用。 - private static readonly ConcurrentDictionary RegistryActivationMetadataCache = + // 卸载安全的进程级缓存:程序集元数据只按弱键复用。 + // 若程序集来自 collectible AssemblyLoadContext,被回收后会重新分析,而不会被静态缓存永久钉住。 + private static readonly WeakKeyCache AssemblyMetadataCache = new(); - // 进程级缓存:对未命中 generated-registry 的程序集,缓存可加载类型列表以避免重复 GetTypes() 扫描。 - private static readonly ConcurrentDictionary> LoadableTypesCache = - new(ReferenceEqualityComparer.Instance); + // 卸载安全的进程级缓存:registry 类型的构造分析可跨容器复用,但不应阻止类型卸载。 + private static readonly WeakKeyCache RegistryActivationMetadataCache = + new(); + + // 卸载安全的进程级缓存:可加载类型列表只在程序集存活期间保留; + // 若程序集卸载,后续重新加载后的首次注册会重新执行 GetTypes()/恢复逻辑。 + private static readonly WeakKeyCache> LoadableTypesCache = + new(); /// /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 diff --git a/GFramework.Cqrs/Internal/WeakKeyCache.cs b/GFramework.Cqrs/Internal/WeakKeyCache.cs new file mode 100644 index 00000000..fffe006b --- /dev/null +++ b/GFramework.Cqrs/Internal/WeakKeyCache.cs @@ -0,0 +1,178 @@ +namespace GFramework.Cqrs.Internal; + +/// +/// 提供基于弱键语义的线程安全缓存。 +/// 该缓存用于跨容器复用与 绑定的派生元数据, +/// 同时避免静态强引用阻止 collectible 程序集或热重载类型被卸载。 +/// +/// 缓存键类型。 +/// 缓存值类型。 +/// +/// 该缓存只保证“命中时复用”,不保证“永久保留”。 +/// 当键对象被 GC 回收后,条目会自然失效,后续访问会重新计算对应值。 +/// 这是 CQRS 运行时在卸载安全与热路径性能之间的显式权衡。 +/// +internal sealed class WeakKeyCache + where TKey : class + where TValue : class +{ + private readonly object _gate = new(); + private ConditionalWeakTable _entries = new(); + + /// + /// 获取指定键对应的缓存值;若当前未命中,则在锁保护下创建并写入。 + /// + /// 缓存键。 + /// 创建缓存值的工厂方法。 + /// 已存在或新创建的缓存值。 + /// + /// 。 + /// + public TValue GetOrAdd(TKey key, Func valueFactory) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(valueFactory); + + var entries = Volatile.Read(ref _entries); + if (entries.TryGetValue(key, out var cachedValue)) + return cachedValue; + + lock (_gate) + { + entries = _entries; + if (entries.TryGetValue(key, out cachedValue)) + return cachedValue; + + var createdValue = valueFactory(key); + ArgumentNullException.ThrowIfNull(createdValue); + entries.Add(key, createdValue); + return createdValue; + } + } + + /// + /// 尝试读取当前缓存中的值,而不触发新的创建逻辑。 + /// + /// 缓存键。 + /// 命中时返回的缓存值。 + /// 若命中当前缓存则为 ;否则为 + /// + public bool TryGetValue(TKey key, out TValue? value) + { + ArgumentNullException.ThrowIfNull(key); + return Volatile.Read(ref _entries).TryGetValue(key, out value); + } + + /// + /// 清空当前缓存实例。 + /// + /// + /// 该方法主要服务于测试,便于在同一进程内隔离不同用例的静态缓存状态。 + /// + public void Clear() + { + lock (_gate) + { + _entries = new ConditionalWeakTable(); + } + } + + /// + /// 返回指定键当前命中的缓存对象;若未命中则返回 。 + /// + /// 缓存键。 + /// 当前缓存对象,或 + /// + /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 + /// + public TValue? GetValueOrDefaultForTesting(TKey key) + { + return TryGetValue(key, out var value) ? value : null; + } +} + +/// +/// 提供以两段 为键的弱引用缓存。 +/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。 +/// +/// 缓存值类型。 +/// +/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用, +/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。 +/// +internal sealed class WeakTypePairCache + where TValue : class +{ + private readonly WeakKeyCache> _entries = new(); + + /// + /// 获取指定类型对对应的缓存值;若未命中则创建并写入。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 创建缓存值的工厂方法。 + /// 已存在或新创建的缓存值。 + /// + /// 或 + /// 。 + /// + public TValue GetOrAdd(Type primaryType, Type secondaryType, Func valueFactory) + { + ArgumentNullException.ThrowIfNull(primaryType); + ArgumentNullException.ThrowIfNull(secondaryType); + ArgumentNullException.ThrowIfNull(valueFactory); + + var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); + return secondaryEntries.GetOrAdd( + secondaryType, + _ => valueFactory(primaryType, secondaryType)); + } + + /// + /// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 命中时返回的缓存值。 + /// 若命中当前缓存则为 ;否则为 + /// + /// 。 + /// + public bool TryGetValue(Type primaryType, Type secondaryType, out TValue? value) + { + ArgumentNullException.ThrowIfNull(primaryType); + ArgumentNullException.ThrowIfNull(secondaryType); + + if (_entries.TryGetValue(primaryType, out var secondaryEntries) && + secondaryEntries is not null) + return secondaryEntries.TryGetValue(secondaryType, out value); + + value = null; + return false; + } + + /// + /// 清空当前缓存实例。 + /// + /// + /// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。 + /// + public void Clear() + { + _entries.Clear(); + } + + /// + /// 返回指定类型对当前命中的缓存对象;若未命中则返回 。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 当前缓存对象,或 + /// + /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 + /// + public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType) + { + return TryGetValue(primaryType, secondaryType, out var value) ? value : null; + } +} diff --git a/README.md b/README.md index 0f8b9a1c..ba5efd3d 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,10 @@ GFramework 采用清晰分层与模块化设计,强调: dotnet add package GeWuYou.GFramework.Core dotnet add package GeWuYou.GFramework.Core.Abstractions +# CQRS +dotnet add package GeWuYou.GFramework.Cqrs +dotnet add package GeWuYou.GFramework.Cqrs.Abstractions + # 游戏扩展 dotnet add package GeWuYou.GFramework.Game dotnet add package GeWuYou.GFramework.Game.Abstractions diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 9f6621f4..257e29c4 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -21,6 +21,18 @@ CQRS(Command Query Responsibility Segregation,命令查询职责分离)是 - 与架构系统深度集成 - 支持流式处理 +## 接入包 + +按模块安装 CQRS runtime;如果希望在编译期生成 handler 注册表,再额外安装对应的 source generator: + +```bash +dotnet add package GeWuYou.GFramework.Cqrs +dotnet add package GeWuYou.GFramework.Cqrs.Abstractions + +# 可选:编译期生成 handler registry,减少冷启动反射扫描 +dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators +``` + ## 核心概念 ### Command(命令) @@ -91,6 +103,9 @@ public class CreatePlayerCommandHandler : AbstractCommandHandler 说明:消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification` 命名空间,而处理器基类位于 +> `GFramework.Cqrs.Cqrs.*` 命名空间。编写最小示例时需要同时引用对应的消息与 handler 命名空间。 + ### Dispatcher(请求分发器) 架构上下文会负责将命令、查询和通知路由到对应的处理器: diff --git a/docs/zh-CN/getting-started/installation.md b/docs/zh-CN/getting-started/installation.md index b99432de..403bf708 100644 --- a/docs/zh-CN/getting-started/installation.md +++ b/docs/zh-CN/getting-started/installation.md @@ -6,16 +6,18 @@ GFramework 提供多种安装方式,您可以根据项目需求选择合适的 GFramework 采用模块化设计,不同包提供不同的功能: -| 包名 | 说明 | 适用场景 | -|---------------------------------------------|-------------|--------------------------------| -| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 | -| `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 | -| `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 | -| `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 | -| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]`、`[ContextAware]`、架构注入等 | -| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema / 配表生成 | -| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 | -| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 | +| 包名 | 说明 | 适用场景 | +|---------------------------------------------|--------------|--------------------------------| +| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 | +| `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 | +| `GeWuYou.GFramework.Cqrs` | CQRS runtime | 命令/查询/通知分发与处理器注册 | +| `GeWuYou.GFramework.Cqrs.Abstractions` | CQRS 抽象契约 | CQRS 契约、handler 接口与共享抽象 | +| `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 | +| `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 | +| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]`、`[ContextAware]`、架构注入等 | +| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema / 配表生成 | +| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 | +| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 | 当前 NuGet 发布按模块拆分 source generator 包,不存在 `GeWuYou.GFramework.SourceGenerators` 聚合包。 @@ -28,6 +30,10 @@ GFramework 采用模块化设计,不同包提供不同的功能: dotnet add package GeWuYou.GFramework.Core dotnet add package GeWuYou.GFramework.Core.Abstractions +# CQRS runtime +dotnet add package GeWuYou.GFramework.Cqrs +dotnet add package GeWuYou.GFramework.Cqrs.Abstractions + # 游戏扩展 dotnet add package GeWuYou.GFramework.Game dotnet add package GeWuYou.GFramework.Game.Abstractions @@ -62,6 +68,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators + + + + @@ -154,12 +164,14 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators 创建一个简单的测试来验证安装是否成功: ```csharp -using GFramework.Core.Architecture; +using GFramework.Core.Architectures; +using GFramework.Core.Model; +using GFramework.Core.Property; // 定义简单的架构 public class TestArchitecture : Architecture { - protected override void Init() + protected override void OnInitialize() { // 注册一个简单的模型 RegisterModel(new TestModel()); @@ -169,6 +181,10 @@ public class TestArchitecture : Architecture public class TestModel : AbstractModel { public BindableProperty Message { get; } = new("Hello GFramework!"); + + protected override void OnInit() + { + } } // 测试代码 diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md index 09d92013..7d11b400 100644 --- a/docs/zh-CN/source-generators/index.md +++ b/docs/zh-CN/source-generators/index.md @@ -11,6 +11,7 @@ GFramework 当前按模块提供一组 Source Generators,通过编译时分析 - [安装配置](#安装配置) - [Log 属性生成器](#log-属性生成器) - [Config Schema 生成器](#config-schema-生成器) +- [CQRS Handler Registry 生成器](#cqrs-handler-registry-生成器) - [ContextAware 属性生成器](#contextaware-属性生成器) - [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器) - [Priority 属性生成器](#priority-属性生成器) @@ -54,6 +55,7 @@ GFramework 的 Source Generators 利用 Roslyn 源代码生成器技术,在编 - **[Log] 属性**:自动生成 ILogger 字段和日志方法 - **Config Schema 生成器**:根据 `*.schema.json` 生成配置类型和表包装 +- **CQRS Handler Registry 生成器**:为 CQRS handlers 生成程序集级注册表并缩小运行时反射范围 - **[ContextAware] 属性**:自动实现 IContextAware 接口 - **[GenerateEnumExtensions] 属性**:自动生成枚举扩展方法 - **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记 @@ -161,6 +163,70 @@ Config Schema 生成器会扫描 `*.schema.json` 文件,并生成: ``` +## CQRS Handler Registry 生成器 + +`GeWuYou.GFramework.Cqrs.SourceGenerators` 会在编译期分析当前业务程序集中的 CQRS handlers,并生成: + +- `ICqrsHandlerRegistry` 实现,用于在启动时直接注册可安全引用的 handlers +- 程序集级 `CqrsHandlerRegistryAttribute` 元数据,供运行时优先走生成注册路径 +- 必要时的 `CqrsReflectionFallbackAttribute`,让运行时只补扫生成代码无法合法引用的 handlers + +### 接入包 + +如果你的项目已经使用 GFramework 架构层,请在现有 Core 依赖基础上补齐 CQRS runtime 与 generator: + +```xml + + + + + +``` + +如果当前项目还没有接入架构运行时,请同时保持 `GeWuYou.GFramework.Core` / +`GeWuYou.GFramework.Core.Abstractions` 与 CQRS 包版本一致。 + +### 最小示例 + +下面的最小示例展示了“安装 runtime + source generator 后,正常注册程序集”的接入方式。运行时会优先使用生成的 +handler registry;如果某个 handler 无法被生成代码直接引用,则自动补走定向反射回退。 + +```csharp +using GFramework.Core.Architectures; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Cqrs.Command; + +public sealed record CreatePlayerCommand(string Name) : ICommand; + +public sealed class CreatePlayerCommandHandler : AbstractCommandHandler +{ + public override ValueTask Handle(CreatePlayerCommand command, CancellationToken cancellationToken) + { + return ValueTask.FromResult(command.Name.Length); + } +} + +public sealed class GameArchitecture : Architecture +{ + protected override void OnInitialize() + { + RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly); + } +} +``` + +### 兼容性与迁移说明 + +- 不安装 `GeWuYou.GFramework.Cqrs.SourceGenerators` 也可以正常运行;此时 CQRS runtime 会继续使用反射扫描注册 + handlers。 +- 安装生成器后,不需要额外改写 `RegisterCqrsHandlersFromAssembly(...)` / + `RegisterCqrsHandlersFromAssemblies(...)` 调用点;运行时会自动优先使用生成注册表。 +- CQRS 消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification`,而处理器基类位于 + `GFramework.Cqrs.Cqrs.*` 命名空间。文档示例需要分别引用两组命名空间。 + ## Log 属性生成器 [Log] 属性自动为标记的类生成日志记录功能,包括 ILogger 字段和便捷的日志方法。 From 45ab38519b0701d65db99b820fdbfb173ffe3f04 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:12:18 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0CQRS?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E5=AE=9E=E7=8E=B0=E5=92=8C=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了完整的CQRS分发器,支持命令、查询、通知和流式处理 - 添加了弱键缓存机制确保程序集卸载安全 - 实现了管道行为支持,可添加日志、验证等横切关注点 - 提供了类型安全的请求分发和处理器注册功能 - 编写了详细的CQRS中文文档,涵盖基本用法和最佳实践 - 支持编译期生成处理器注册表优化启动性能 --- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 37 ++++++++++++++++++++++ GFramework.Cqrs/Internal/WeakKeyCache.cs | 36 ++++++++++++++++++++- docs/zh-CN/core/cqrs.md | 1 + 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index d229d190..5f7fa643 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -387,32 +387,69 @@ internal sealed class CqrsDispatcher( } } + /// + /// 保存通知分发路径所需的服务类型与强类型调用委托。 + /// 该绑定把“容器解析哪个服务类型”与“如何调用处理器”聚合到同一缓存项中。 + /// private sealed class NotificationDispatchBinding(Type handlerType, NotificationInvoker invoker) { + /// + /// 获取通知处理器在容器中的服务类型。 + /// public Type HandlerType { get; } = handlerType; + /// + /// 获取执行通知处理器的强类型调用委托。 + /// public NotificationInvoker Invoker { get; } = invoker; } + /// + /// 保存流式请求分发路径所需的服务类型与调用委托。 + /// 该绑定让建流热路径只需一次缓存命中即可获得解析与调用所需元数据。 + /// private sealed class StreamDispatchBinding(Type handlerType, StreamInvoker invoker) { + /// + /// 获取流式请求处理器在容器中的服务类型。 + /// public Type HandlerType { get; } = handlerType; + /// + /// 获取执行流式请求处理器的调用委托。 + /// public StreamInvoker Invoker { get; } = invoker; } + /// + /// 保存普通请求分发路径所需的 handler 服务类型、pipeline 服务类型与强类型调用委托。 + /// 该绑定同时覆盖“直接请求处理”和“带 pipeline 的请求处理”两条路径。 + /// + /// 请求响应类型。 private sealed class RequestDispatchBinding( Type handlerType, Type behaviorType, RequestInvoker requestInvoker, RequestPipelineInvoker pipelineInvoker) { + /// + /// 获取请求处理器在容器中的服务类型。 + /// public Type HandlerType { get; } = handlerType; + /// + /// 获取 pipeline 行为在容器中的服务类型。 + /// public Type BehaviorType { get; } = behaviorType; + /// + /// 获取直接调用请求处理器的强类型委托。 + /// public RequestInvoker RequestInvoker { get; } = requestInvoker; + /// + /// 获取执行 pipeline 行为链的强类型委托。 + /// public RequestPipelineInvoker PipelineInvoker { get; } = pipelineInvoker; } } diff --git a/GFramework.Cqrs/Internal/WeakKeyCache.cs b/GFramework.Cqrs/Internal/WeakKeyCache.cs index fffe006b..7377c2ca 100644 --- a/GFramework.Cqrs/Internal/WeakKeyCache.cs +++ b/GFramework.Cqrs/Internal/WeakKeyCache.cs @@ -50,6 +50,39 @@ internal sealed class WeakKeyCache } } + /// + /// 获取指定键对应的缓存值;若当前未命中,则在锁保护下使用附加状态创建并写入。 + /// + /// 创建缓存值时需要携带的附加状态类型。 + /// 缓存键。 + /// 创建缓存值时复用的附加状态。 + /// 基于键与附加状态创建缓存值的工厂方法。 + /// 已存在或新创建的缓存值。 + /// + /// 。 + /// + public TValue GetOrAdd(TKey key, TState state, Func valueFactory) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(valueFactory); + + var entries = Volatile.Read(ref _entries); + if (entries.TryGetValue(key, out var cachedValue)) + return cachedValue; + + lock (_gate) + { + entries = _entries; + if (entries.TryGetValue(key, out cachedValue)) + return cachedValue; + + var createdValue = valueFactory(key, state); + ArgumentNullException.ThrowIfNull(createdValue); + entries.Add(key, createdValue); + return createdValue; + } + } + /// /// 尝试读取当前缓存中的值,而不触发新的创建逻辑。 /// @@ -125,7 +158,8 @@ internal sealed class WeakTypePairCache var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); return secondaryEntries.GetOrAdd( secondaryType, - _ => valueFactory(primaryType, secondaryType)); + (PrimaryType: primaryType, Factory: valueFactory), + static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType)); } /// diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 257e29c4..0c248a96 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -83,6 +83,7 @@ public class GetPlayerQuery : QueryBase 处理器负责执行命令或查询的具体逻辑: ```csharp +using GFramework.Cqrs.Command; using GFramework.Cqrs.Cqrs.Command; // 命令处理器 From 3be34156cf2e14ad367b1aa1f2cdae07b0b230ea Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:17:46 +0800 Subject: [PATCH 6/7] =?UTF-8?q?docs(cache):=20=E6=9B=B4=E6=96=B0=20WeakKey?= =?UTF-8?q?Cache=20=E5=BC=82=E5=B8=B8=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 GetOrAdd 方法添加 valueFactory 返回 null 时的异常说明 - 在 GlobalUsings 中添加 System.Runtime.CompilerServices 引用 --- GFramework.Cqrs/GlobalUsings.cs | 1 + GFramework.Cqrs/Internal/WeakKeyCache.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs index 3085d1e1..1e6e7889 100644 --- a/GFramework.Cqrs/GlobalUsings.cs +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -7,3 +7,4 @@ global using System.Reflection; global using Microsoft.Extensions.DependencyInjection; global using System.Diagnostics; global using System.Collections.Concurrent; +global using System.Runtime.CompilerServices; diff --git a/GFramework.Cqrs/Internal/WeakKeyCache.cs b/GFramework.Cqrs/Internal/WeakKeyCache.cs index 7377c2ca..dadf2db4 100644 --- a/GFramework.Cqrs/Internal/WeakKeyCache.cs +++ b/GFramework.Cqrs/Internal/WeakKeyCache.cs @@ -27,6 +27,7 @@ internal sealed class WeakKeyCache /// 已存在或新创建的缓存值。 /// /// 。 + /// 或 返回 。 /// public TValue GetOrAdd(TKey key, Func valueFactory) { @@ -60,6 +61,7 @@ internal sealed class WeakKeyCache /// 已存在或新创建的缓存值。 /// /// 。 + /// 或 返回 。 /// public TValue GetOrAdd(TKey key, TState state, Func valueFactory) { From 010453761566606c6c3248782c6a7fd780984127 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:19:25 +0800 Subject: [PATCH 7/7] =?UTF-8?q?refactor(config):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E9=85=8D=E7=BD=AE=E9=9B=86=E6=88=90=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=B8=AD=E7=9A=84=E8=AD=A6=E5=91=8A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在初始化方法中添加了针对GF_ContextRegistration_003警告的禁用指令 - 通过#pragma warning disable和#pragma warning restore临时禁用了特定警告 - 保持了原有的配置注册表获取和怪物表格数据读取逻辑 - 确保了测试代码的编译清洁性同时维持功能完整性 --- .../Config/ArchitectureConfigIntegrationTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs index f9a2492f..3020f9fd 100644 --- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs +++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs @@ -325,8 +325,8 @@ public class ArchitectureConfigIntegrationTests } /// - /// 创建一个使用配置模块的模块实例。 - /// + /// 创建一个使用配置模块的模块实例。 + /// /// 测试配置根目录。 /// 已配置的模块实例。 private static GameConfigModule CreateModule(string configRoot) @@ -529,7 +529,9 @@ public class ArchitectureConfigIntegrationTests /// protected override void OnInit() { +#pragma warning disable GF_ContextRegistration_003 var registry = this.GetUtility(); +#pragma warning restore GF_ContextRegistration_003 var monsterTable = registry.GetMonsterTable(); ObservedMonsterName = monsterTable.Get(1).Name;