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 字段和便捷的日志方法。