docs: 添加 CQRS 架构模式与源码生成器完整文档

- 新增 CQRS 核心概念、命令查询处理器实现指南
- 添加安装配置文档,包含各模块包说明与环境要求
- 实现源码生成器全面文档,涵盖 Log、Config Schema、CQRS Handler Registry 等特性
- 提供详细用法示例与最佳实践指导
- 包含常见问题解答与故障排查方案
- 添加 Godot 项目集成与性能优化相关内容
This commit is contained in:
GeWuYou 2026-04-17 17:41:38 +08:00
parent 38f98ea7ea
commit 04123d2a71
9 changed files with 528 additions and 95 deletions

View File

@ -12,6 +12,9 @@ namespace GFramework.Cqrs.Tests.Cqrs;
[TestFixture] [TestFixture]
internal sealed class CqrsDispatcherCacheTests internal sealed class CqrsDispatcherCacheTests
{ {
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary> /// <summary>
/// 初始化测试上下文。 /// 初始化测试上下文。
/// </summary> /// </summary>
@ -42,9 +45,6 @@ internal sealed class CqrsDispatcherCacheTests
_container = null; _container = null;
} }
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary> /// <summary>
/// 验证相同消息类型重复分发时,不会重复扩张 dispatch binding 缓存。 /// 验证相同消息类型重复分发时,不会重复扩张 dispatch binding 缓存。
/// </summary> /// </summary>
@ -52,21 +52,38 @@ internal sealed class CqrsDispatcherCacheTests
public async Task Dispatcher_Should_Cache_Dispatch_Bindings_After_First_Dispatch() public async Task Dispatcher_Should_Cache_Dispatch_Bindings_After_First_Dispatch()
{ {
var notificationBindings = GetCacheField("NotificationDispatchBindings"); var notificationBindings = GetCacheField("NotificationDispatchBindings");
var requestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings"); var requestBindings = GetCacheField("RequestDispatchBindings");
var streamBindings = GetCacheField("StreamDispatchBindings"); var streamBindings = GetCacheField("StreamDispatchBindings");
var notificationBefore = notificationBindings.Count; Assert.Multiple(() =>
var requestBefore = requestBindings.Count; {
var streamBefore = streamBindings.Count; 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 DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
await _context.PublishAsync(new DispatcherCacheNotification()); await _context.PublishAsync(new DispatcherCacheNotification());
await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest())); await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest()));
var notificationAfterFirstDispatch = notificationBindings.Count; var notificationAfterFirstDispatch =
var requestAfterFirstDispatch = requestBindings.Count; GetSingleKeyCacheValue(notificationBindings, typeof(DispatcherCacheNotification));
var streamAfterFirstDispatch = streamBindings.Count; 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 DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest()); await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
@ -75,13 +92,23 @@ internal sealed class CqrsDispatcherCacheTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(notificationAfterFirstDispatch, Is.EqualTo(notificationBefore + 1)); Assert.That(notificationAfterFirstDispatch, Is.Not.Null);
Assert.That(requestAfterFirstDispatch, Is.EqualTo(requestBefore + 2)); Assert.That(requestAfterFirstDispatch, Is.Not.Null);
Assert.That(streamAfterFirstDispatch, Is.EqualTo(streamBefore + 1)); Assert.That(pipelineAfterFirstDispatch, Is.Not.Null);
Assert.That(streamAfterFirstDispatch, Is.Not.Null);
Assert.That(notificationBindings.Count, Is.EqualTo(notificationAfterFirstDispatch)); Assert.That(
Assert.That(requestBindings.Count, Is.EqualTo(requestAfterFirstDispatch)); GetSingleKeyCacheValue(notificationBindings, typeof(DispatcherCacheNotification)),
Assert.That(streamBindings.Count, Is.EqualTo(streamAfterFirstDispatch)); 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] [Test]
public async Task Dispatcher_Should_Cache_Request_Dispatch_Bindings_Per_Response_Type() public async Task Dispatcher_Should_Cache_Request_Dispatch_Bindings_Per_Response_Type()
{ {
var intRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings"); var requestBindings = GetCacheField("RequestDispatchBindings");
var stringRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings");
var intBefore = intRequestBindings.Count;
var stringBefore = stringRequestBindings.Count;
await _context!.SendRequestAsync(new DispatcherCacheRequest()); await _context!.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherStringCacheRequest()); await _context.SendRequestAsync(new DispatcherStringCacheRequest());
var intAfterFirstDispatch = intRequestBindings.Count; var intAfterFirstDispatch =
var stringAfterFirstDispatch = stringRequestBindings.Count; GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int));
var stringAfterFirstDispatch =
GetPairCacheValue(requestBindings, typeof(DispatcherStringCacheRequest), typeof(string));
await _context.SendRequestAsync(new DispatcherCacheRequest()); await _context.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherStringCacheRequest()); await _context.SendRequestAsync(new DispatcherStringCacheRequest());
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(intAfterFirstDispatch, Is.EqualTo(intBefore + 1)); Assert.That(intAfterFirstDispatch, Is.Not.Null);
Assert.That(stringAfterFirstDispatch, Is.EqualTo(stringBefore + 1)); Assert.That(stringAfterFirstDispatch, Is.Not.Null);
Assert.That(intRequestBindings.Count, Is.EqualTo(intAfterFirstDispatch)); Assert.That(intAfterFirstDispatch, Is.Not.SameAs(stringAfterFirstDispatch));
Assert.That(stringRequestBindings.Count, Is.EqualTo(stringAfterFirstDispatch)); Assert.That(
GetPairCacheValue(requestBindings, typeof(DispatcherCacheRequest), typeof(int)),
Is.SameAs(intAfterFirstDispatch));
Assert.That(
GetPairCacheValue(requestBindings, typeof(DispatcherStringCacheRequest), typeof(string)),
Is.SameAs(stringAfterFirstDispatch));
}); });
} }
/// <summary> /// <summary>
/// 通过反射读取 dispatcher 的静态缓存字典。 /// 通过反射读取 dispatcher 的静态缓存对象
/// </summary> /// </summary>
private static IDictionary GetCacheField(string fieldName) private static object GetCacheField(string fieldName)
{ {
var dispatcherType = GetDispatcherType(); var dispatcherType = GetDispatcherType();
var field = dispatcherType.GetField( var field = dispatcherType.GetField(
@ -127,9 +157,9 @@ internal sealed class CqrsDispatcherCacheTests
Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}."); Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}.");
return field!.GetValue(null) as IDictionary return field!.GetValue(null)
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
$"Dispatcher cache field {fieldName} does not implement IDictionary."); $"Dispatcher cache field {fieldName} returned null.");
} }
/// <summary> /// <summary>
@ -137,36 +167,47 @@ internal sealed class CqrsDispatcherCacheTests
/// </summary> /// </summary>
private static void ClearDispatcherCaches() private static void ClearDispatcherCaches()
{ {
GetCacheField("NotificationDispatchBindings").Clear(); ClearCache(GetCacheField("NotificationDispatchBindings"));
GetCacheField("StreamDispatchBindings").Clear(); ClearCache(GetCacheField("RequestDispatchBindings"));
GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings").Clear(); ClearCache(GetCacheField("StreamDispatchBindings"));
GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings").Clear();
} }
/// <summary> /// <summary>
/// 通过反射读取 dispatcher 嵌套泛型缓存类型上的静态缓存字典 /// 读取单键缓存中当前保存的对象
/// </summary> /// </summary>
private static IDictionary GetGenericCacheField(string nestedTypeName, Type genericTypeArgument, string fieldName) private static object? GetSingleKeyCacheValue(object cache, Type key)
{ {
var nestedGenericType = GetDispatcherType().GetNestedType( return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", key);
nestedTypeName, }
BindingFlags.NonPublic);
Assert.That(nestedGenericType, Is.Not.Null, $"Missing dispatcher nested cache type {nestedTypeName}."); /// <summary>
/// 读取双键缓存中当前保存的对象。
/// </summary>
private static object? GetPairCacheValue(object cache, Type primaryType, Type secondaryType)
{
return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType);
}
var closedNestedType = nestedGenericType!.MakeGenericType(genericTypeArgument); /// <summary>
var field = closedNestedType.GetField( /// 调用缓存实例上的无参清理方法。
fieldName, /// </summary>
BindingFlags.NonPublic | BindingFlags.Static); private static void ClearCache(object cache)
{
_ = InvokeInstanceMethod(cache, "Clear");
}
Assert.That( /// <summary>
field, /// 调用缓存对象上的实例方法。
Is.Not.Null, /// </summary>
$"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 Assert.That(method, Is.Not.Null, $"Missing cache method {target.GetType().FullName}.{methodName}.");
?? throw new InvalidOperationException(
$"Dispatcher nested cache field {nestedTypeName}.{fieldName} does not implement IDictionary."); return method!.Invoke(target, arguments);
} }
/// <summary> /// <summary>

View File

@ -326,6 +326,33 @@ internal sealed class CqrsHandlerRegistrarTests
CqrsTestRuntime.RegisterHandlers(firstContainer, generatedAssembly.Object); CqrsTestRuntime.RegisterHandlers(firstContainer, generatedAssembly.Object);
CqrsTestRuntime.RegisterHandlers(secondContainer, generatedAssembly.Object); CqrsTestRuntime.RegisterHandlers(secondContainer, generatedAssembly.Object);
firstContainer.Freeze();
secondContainer.Freeze();
var firstRegistrations = firstContainer.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();
var secondRegistrations = secondContainer.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.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( generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false), static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false),

View File

@ -15,14 +15,20 @@ internal sealed class CqrsDispatcher(
IIocContainer container, IIocContainer container,
ILogger logger) : ICqrsRuntime ILogger logger) : ICqrsRuntime
{ {
// 进程级缓存:把通知服务类型与调用委托绑定到同一项,减少发布热路径上的重复字典查询。 // 卸载安全的进程级缓存:通知类型只以弱键语义保留。
private static readonly ConcurrentDictionary<Type, NotificationDispatchBinding> // 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
NotificationDispatchBindings = new(); NotificationDispatchBindings = new();
// 进程级缓存:把流式处理器服务类型与调用委托绑定到同一项,减少建流热路径上的重复字典查询 // 卸载安全的进程级缓存:请求/响应类型对采用弱键缓存,避免流式消息类型被静态字典永久保留
private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamDispatchBinding> private static readonly WeakTypePairCache<StreamDispatchBinding>
StreamDispatchBindings = new(); StreamDispatchBindings = new();
// 卸载安全的进程级缓存:请求/响应类型对命中后复用强类型 dispatch binding
// 若任一类型被回收,后续首次发送时会按当前加载状态重新生成。
private static readonly WeakTypePairCache<RequestDispatchBindingBox>
RequestDispatchBindings = new();
// 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。 // 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。
private static readonly MethodInfo RequestHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) private static readonly MethodInfo RequestHandlerInvokerMethodDefinition = typeof(CqrsDispatcher)
.GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
@ -88,9 +94,7 @@ internal sealed class CqrsDispatcher(
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
var requestType = request.GetType(); var requestType = request.GetType();
var dispatchBinding = RequestDispatchBindingCache<TResponse>.Bindings.GetOrAdd( var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
requestType,
CreateRequestDispatchBinding<TResponse>);
var handler = container.Get(dispatchBinding.HandlerType) var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
$"No CQRS request handler registered for {requestType.FullName}."); $"No CQRS request handler registered for {requestType.FullName}.");
@ -125,8 +129,9 @@ internal sealed class CqrsDispatcher(
var requestType = request.GetType(); var requestType = request.GetType();
var dispatchBinding = StreamDispatchBindings.GetOrAdd( var dispatchBinding = StreamDispatchBindings.GetOrAdd(
(requestType, typeof(TResponse)), requestType,
static key => CreateStreamDispatchBinding(key.RequestType, key.ResponseType)); typeof(TResponse),
CreateStreamDispatchBinding);
var handler = container.Get(dispatchBinding.HandlerType) var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
$"No CQRS stream handler registered for {requestType.FullName}."); $"No CQRS stream handler registered for {requestType.FullName}.");
@ -165,6 +170,32 @@ internal sealed class CqrsDispatcher(
CreateRequestPipelineInvoker<TResponse>(requestType)); CreateRequestPipelineInvoker<TResponse>(requestType));
} }
/// <summary>
/// 获取指定请求/响应类型对的 dispatch binding若缓存未命中则按当前加载状态创建。
/// </summary>
private static RequestDispatchBinding<TResponse> GetRequestDispatchBinding<TResponse>(Type requestType)
{
var bindingBox = RequestDispatchBindings.GetOrAdd(
requestType,
typeof(TResponse),
CreateRequestDispatchBindingBox<TResponse>);
return bindingBox.Get<TResponse>();
}
/// <summary>
/// 为弱键请求缓存创建强类型 binding 盒子,避免 value-type 响应走 object 结果桥接。
/// </summary>
private static RequestDispatchBindingBox CreateRequestDispatchBindingBox<TResponse>(
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<TResponse>(requestType));
}
/// <summary> /// <summary>
/// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。 /// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。
/// </summary> /// </summary>
@ -312,22 +343,76 @@ internal sealed class CqrsDispatcher(
private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// 按响应类型分层缓存 request 分发绑定,既避免 value-type 响应走 object 桥接 /// 将不同响应类型的 request dispatch binding 包装到统一弱缓存值中
/// 也让 handler/pipeline 服务类型与调用委托在热路径上只命中一次缓存 /// 同时保留强类型委托,避免值类型响应退化为 object 桥接
/// </summary> /// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam> private abstract class RequestDispatchBindingBox
private static class RequestDispatchBindingCache<TResponse>
{ {
internal static readonly ConcurrentDictionary<Type, RequestDispatchBinding<TResponse>> Bindings = new(); /// <summary>
/// 创建一个新的强类型 dispatch binding 盒子。
/// </summary>
public static RequestDispatchBindingBox Create<TResponse>(RequestDispatchBinding<TResponse> binding)
{
ArgumentNullException.ThrowIfNull(binding);
return new RequestDispatchBindingBox<TResponse>(binding);
}
/// <summary>
/// 读取指定响应类型的 request dispatch binding。
/// </summary>
public abstract RequestDispatchBinding<TResponse> Get<TResponse>();
} }
private readonly record struct NotificationDispatchBinding(Type HandlerType, NotificationInvoker Invoker); /// <summary>
/// 保存特定响应类型的 request dispatch binding。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
private sealed class RequestDispatchBindingBox<TResponse>(RequestDispatchBinding<TResponse> binding)
: RequestDispatchBindingBox
{
private readonly RequestDispatchBinding<TResponse> _binding = binding;
private readonly record struct StreamDispatchBinding(Type HandlerType, StreamInvoker Invoker); /// <summary>
/// 以原始强类型返回当前 binding若请求的响应类型不匹配则抛出异常。
/// </summary>
public override RequestDispatchBinding<TRequestedResponse> Get<TRequestedResponse>()
{
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<TResponse>( return (RequestDispatchBinding<TRequestedResponse>)(object)_binding;
Type HandlerType, }
Type BehaviorType, }
RequestInvoker<TResponse> RequestInvoker,
RequestPipelineInvoker<TResponse> PipelineInvoker); 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<TResponse>(
Type handlerType,
Type behaviorType,
RequestInvoker<TResponse> requestInvoker,
RequestPipelineInvoker<TResponse> pipelineInvoker)
{
public Type HandlerType { get; } = handlerType;
public Type BehaviorType { get; } = behaviorType;
public RequestInvoker<TResponse> RequestInvoker { get; } = requestInvoker;
public RequestPipelineInvoker<TResponse> PipelineInvoker { get; } = pipelineInvoker;
}
} }

View File

@ -11,18 +11,19 @@ namespace GFramework.Cqrs.Internal;
/// </summary> /// </summary>
internal static class CqrsHandlerRegistrar internal static class CqrsHandlerRegistrar
{ {
// 进程级缓存:同一程序集的 generated-registry 元数据与 reflection-fallback 元数据在加载后保持稳定, // 卸载安全的进程级缓存:程序集元数据只按弱键复用。
// 因此可跨容器复用分析结果,避免每次注册都重复读取程序集级 attribute。 // 若程序集来自 collectible AssemblyLoadContext被回收后会重新分析而不会被静态缓存永久钉住。
private static readonly ConcurrentDictionary<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache = private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
new(ReferenceEqualityComparer.Instance);
// 进程级缓存registry 类型的可激活性与构造入口是稳定的,可跨多次容器初始化复用。
private static readonly ConcurrentDictionary<Type, RegistryActivationMetadata> RegistryActivationMetadataCache =
new(); new();
// 进程级缓存:对未命中 generated-registry 的程序集,缓存可加载类型列表以避免重复 GetTypes() 扫描。 // 卸载安全的进程级缓存registry 类型的构造分析可跨容器复用,但不应阻止类型卸载。
private static readonly ConcurrentDictionary<Assembly, IReadOnlyList<Type>> LoadableTypesCache = private static readonly WeakKeyCache<Type, RegistryActivationMetadata> RegistryActivationMetadataCache =
new(ReferenceEqualityComparer.Instance); new();
// 卸载安全的进程级缓存:可加载类型列表只在程序集存活期间保留;
// 若程序集卸载,后续重新加载后的首次注册会重新执行 GetTypes()/恢复逻辑。
private static readonly WeakKeyCache<Assembly, IReadOnlyList<Type>> LoadableTypesCache =
new();
/// <summary> /// <summary>
/// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。

View File

@ -0,0 +1,178 @@
namespace GFramework.Cqrs.Internal;
/// <summary>
/// 提供基于弱键语义的线程安全缓存。
/// 该缓存用于跨容器复用与 <see cref="Assembly" /> 或 <see cref="Type" /> 绑定的派生元数据,
/// 同时避免静态强引用阻止 collectible 程序集或热重载类型被卸载。
/// </summary>
/// <typeparam name="TKey">缓存键类型。</typeparam>
/// <typeparam name="TValue">缓存值类型。</typeparam>
/// <remarks>
/// 该缓存只保证“命中时复用”,不保证“永久保留”。
/// 当键对象被 GC 回收后,条目会自然失效,后续访问会重新计算对应值。
/// 这是 CQRS 运行时在卸载安全与热路径性能之间的显式权衡。
/// </remarks>
internal sealed class WeakKeyCache<TKey, TValue>
where TKey : class
where TValue : class
{
private readonly object _gate = new();
private ConditionalWeakTable<TKey, TValue> _entries = new();
/// <summary>
/// 获取指定键对应的缓存值;若当前未命中,则在锁保护下创建并写入。
/// </summary>
/// <param name="key">缓存键。</param>
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
/// <returns>已存在或新创建的缓存值。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。
/// </exception>
public TValue GetOrAdd(TKey key, Func<TKey, TValue> 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;
}
}
/// <summary>
/// 尝试读取当前缓存中的值,而不触发新的创建逻辑。
/// </summary>
/// <param name="key">缓存键。</param>
/// <param name="value">命中时返回的缓存值。</param>
/// <returns>若命中当前缓存则为 <see langword="true" />;否则为 <see langword="false" />。</returns>
/// <exception cref="ArgumentNullException"><paramref name="key" /> 为 <see langword="null" />。</exception>
public bool TryGetValue(TKey key, out TValue? value)
{
ArgumentNullException.ThrowIfNull(key);
return Volatile.Read(ref _entries).TryGetValue(key, out value);
}
/// <summary>
/// 清空当前缓存实例。
/// </summary>
/// <remarks>
/// 该方法主要服务于测试,便于在同一进程内隔离不同用例的静态缓存状态。
/// </remarks>
public void Clear()
{
lock (_gate)
{
_entries = new ConditionalWeakTable<TKey, TValue>();
}
}
/// <summary>
/// 返回指定键当前命中的缓存对象;若未命中则返回 <see langword="null" />。
/// </summary>
/// <param name="key">缓存键。</param>
/// <returns>当前缓存对象,或 <see langword="null" />。</returns>
/// <remarks>
/// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。
/// </remarks>
public TValue? GetValueOrDefaultForTesting(TKey key)
{
return TryGetValue(key, out var value) ? value : null;
}
}
/// <summary>
/// 提供以两段 <see cref="Type" /> 为键的弱引用缓存。
/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。
/// </summary>
/// <typeparam name="TValue">缓存值类型。</typeparam>
/// <remarks>
/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用,
/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。
/// </remarks>
internal sealed class WeakTypePairCache<TValue>
where TValue : class
{
private readonly WeakKeyCache<Type, WeakKeyCache<Type, TValue>> _entries = new();
/// <summary>
/// 获取指定类型对对应的缓存值;若未命中则创建并写入。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
/// <returns>已存在或新创建的缓存值。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="primaryType" />、<paramref name="secondaryType" /> 或
/// <paramref name="valueFactory" /> 为 <see langword="null" />。
/// </exception>
public TValue GetOrAdd(Type primaryType, Type secondaryType, Func<Type, Type, TValue> valueFactory)
{
ArgumentNullException.ThrowIfNull(primaryType);
ArgumentNullException.ThrowIfNull(secondaryType);
ArgumentNullException.ThrowIfNull(valueFactory);
var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache<Type, TValue>());
return secondaryEntries.GetOrAdd(
secondaryType,
_ => valueFactory(primaryType, secondaryType));
}
/// <summary>
/// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <param name="value">命中时返回的缓存值。</param>
/// <returns>若命中当前缓存则为 <see langword="true" />;否则为 <see langword="false" />。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="primaryType" /> 或 <paramref name="secondaryType" /> 为 <see langword="null" />。
/// </exception>
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;
}
/// <summary>
/// 清空当前缓存实例。
/// </summary>
/// <remarks>
/// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。
/// </remarks>
public void Clear()
{
_entries.Clear();
}
/// <summary>
/// 返回指定类型对当前命中的缓存对象;若未命中则返回 <see langword="null" />。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <returns>当前缓存对象,或 <see langword="null" />。</returns>
/// <remarks>
/// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。
/// </remarks>
public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType)
{
return TryGetValue(primaryType, secondaryType, out var value) ? value : null;
}
}

View File

@ -63,6 +63,10 @@ GFramework 采用清晰分层与模块化设计,强调:
dotnet add package GeWuYou.GFramework.Core dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions 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
dotnet add package GeWuYou.GFramework.Game.Abstractions dotnet add package GeWuYou.GFramework.Game.Abstractions

View File

@ -21,6 +21,18 @@ CQRSCommand 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命令 ### Command命令
@ -91,6 +103,9 @@ public class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCom
} }
``` ```
> 说明:消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification` 命名空间,而处理器基类位于
> `GFramework.Cqrs.Cqrs.*` 命名空间。编写最小示例时需要同时引用对应的消息与 handler 命名空间。
### Dispatcher请求分发器 ### Dispatcher请求分发器
架构上下文会负责将命令、查询和通知路由到对应的处理器: 架构上下文会负责将命令、查询和通知路由到对应的处理器:

View File

@ -6,16 +6,18 @@ GFramework 提供多种安装方式,您可以根据项目需求选择合适的
GFramework 采用模块化设计,不同包提供不同的功能: GFramework 采用模块化设计,不同包提供不同的功能:
| 包名 | 说明 | 适用场景 | | 包名 | 说明 | 适用场景 |
|---------------------------------------------|-------------|--------------------------------| |---------------------------------------------|--------------|--------------------------------|
| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 | | `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 |
| `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 | | `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 |
| `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 | | `GeWuYou.GFramework.Cqrs` | CQRS runtime | 命令/查询/通知分发与处理器注册 |
| `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 | | `GeWuYou.GFramework.Cqrs.Abstractions` | CQRS 抽象契约 | CQRS 契约、handler 接口与共享抽象 |
| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]``[ContextAware]`、架构注入等 | | `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 |
| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema / 配表生成 | | `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 |
| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 | | `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]``[ContextAware]`、架构注入等 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 | | `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema / 配表生成 |
| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 |
当前 NuGet 发布按模块拆分 source generator 包,不存在 `GeWuYou.GFramework.SourceGenerators` 聚合包。 当前 NuGet 发布按模块拆分 source generator 包,不存在 `GeWuYou.GFramework.SourceGenerators` 聚合包。
@ -28,6 +30,10 @@ GFramework 采用模块化设计,不同包提供不同的功能:
dotnet add package GeWuYou.GFramework.Core dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions 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
dotnet add package GeWuYou.GFramework.Game.Abstractions dotnet add package GeWuYou.GFramework.Game.Abstractions
@ -63,6 +69,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
<PackageReference Include="GeWuYou.GFramework.Core" Version="1.0.0" /> <PackageReference Include="GeWuYou.GFramework.Core" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Core.Abstractions" Version="1.0.0" /> <PackageReference Include="GeWuYou.GFramework.Core.Abstractions" Version="1.0.0" />
<!-- CQRS runtime -->
<PackageReference Include="GeWuYou.GFramework.Cqrs" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Cqrs.Abstractions" Version="1.0.0" />
<!-- 游戏模块 --> <!-- 游戏模块 -->
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" /> <PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Game.Abstractions" Version="1.0.0" /> <PackageReference Include="GeWuYou.GFramework.Game.Abstractions" Version="1.0.0" />
@ -154,12 +164,14 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
创建一个简单的测试来验证安装是否成功: 创建一个简单的测试来验证安装是否成功:
```csharp ```csharp
using GFramework.Core.Architecture; using GFramework.Core.Architectures;
using GFramework.Core.Model;
using GFramework.Core.Property;
// 定义简单的架构 // 定义简单的架构
public class TestArchitecture : Architecture public class TestArchitecture : Architecture
{ {
protected override void Init() protected override void OnInitialize()
{ {
// 注册一个简单的模型 // 注册一个简单的模型
RegisterModel(new TestModel()); RegisterModel(new TestModel());
@ -169,6 +181,10 @@ public class TestArchitecture : Architecture
public class TestModel : AbstractModel public class TestModel : AbstractModel
{ {
public BindableProperty<string> Message { get; } = new("Hello GFramework!"); public BindableProperty<string> Message { get; } = new("Hello GFramework!");
protected override void OnInit()
{
}
} }
// 测试代码 // 测试代码

View File

@ -11,6 +11,7 @@ GFramework 当前按模块提供一组 Source Generators通过编译时分析
- [安装配置](#安装配置) - [安装配置](#安装配置)
- [Log 属性生成器](#log-属性生成器) - [Log 属性生成器](#log-属性生成器)
- [Config Schema 生成器](#config-schema-生成器) - [Config Schema 生成器](#config-schema-生成器)
- [CQRS Handler Registry 生成器](#cqrs-handler-registry-生成器)
- [ContextAware 属性生成器](#contextaware-属性生成器) - [ContextAware 属性生成器](#contextaware-属性生成器)
- [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器) - [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器)
- [Priority 属性生成器](#priority-属性生成器) - [Priority 属性生成器](#priority-属性生成器)
@ -54,6 +55,7 @@ GFramework 的 Source Generators 利用 Roslyn 源代码生成器技术,在编
- **[Log] 属性**:自动生成 ILogger 字段和日志方法 - **[Log] 属性**:自动生成 ILogger 字段和日志方法
- **Config Schema 生成器**:根据 `*.schema.json` 生成配置类型和表包装 - **Config Schema 生成器**:根据 `*.schema.json` 生成配置类型和表包装
- **CQRS Handler Registry 生成器**:为 CQRS handlers 生成程序集级注册表并缩小运行时反射范围
- **[ContextAware] 属性**:自动实现 IContextAware 接口 - **[ContextAware] 属性**:自动实现 IContextAware 接口
- **[GenerateEnumExtensions] 属性**:自动生成枚举扩展方法 - **[GenerateEnumExtensions] 属性**:自动生成枚举扩展方法
- **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记 - **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记
@ -161,6 +163,70 @@ Config Schema 生成器会扫描 `*.schema.json` 文件,并生成:
</Project> </Project>
``` ```
## CQRS Handler Registry 生成器
`GeWuYou.GFramework.Cqrs.SourceGenerators` 会在编译期分析当前业务程序集中的 CQRS handlers并生成
- `ICqrsHandlerRegistry` 实现,用于在启动时直接注册可安全引用的 handlers
- 程序集级 `CqrsHandlerRegistryAttribute` 元数据,供运行时优先走生成注册路径
- 必要时的 `CqrsReflectionFallbackAttribute`,让运行时只补扫生成代码无法合法引用的 handlers
### 接入包
如果你的项目已经使用 GFramework 架构层,请在现有 Core 依赖基础上补齐 CQRS runtime 与 generator
```xml
<ItemGroup>
<PackageReference Include="GeWuYou.GFramework.Cqrs" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Cqrs.Abstractions" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Cqrs.SourceGenerators"
Version="1.0.0"
PrivateAssets="all"
ExcludeAssets="runtime" />
</ItemGroup>
```
如果当前项目还没有接入架构运行时,请同时保持 `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<int>;
public sealed class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCommand, int>
{
public override ValueTask<int> 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 属性生成器
[Log] 属性自动为标记的类生成日志记录功能,包括 ILogger 字段和便捷的日志方法。 [Log] 属性自动为标记的类生成日志记录功能,包括 ILogger 字段和便捷的日志方法。