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