mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
docs: 添加 CQRS 架构模式与源码生成器完整文档
- 新增 CQRS 核心概念、命令查询处理器实现指南 - 添加安装配置文档,包含各模块包说明与环境要求 - 实现源码生成器全面文档,涵盖 Log、Config Schema、CQRS Handler Registry 等特性 - 提供详细用法示例与最佳实践指导 - 包含常见问题解答与故障排查方案 - 添加 Godot 项目集成与性能优化相关内容
This commit is contained in:
parent
38f98ea7ea
commit
04123d2a71
@ -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>
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 请求/通知/流式处理器。
|
||||||
|
|||||||
178
GFramework.Cqrs/Internal/WeakKeyCache.cs
Normal file
178
GFramework.Cqrs/Internal/WeakKeyCache.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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(命令)
|
### Command(命令)
|
||||||
@ -91,6 +103,9 @@ public class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCom
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 说明:消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification` 命名空间,而处理器基类位于
|
||||||
|
> `GFramework.Cqrs.Cqrs.*` 命名空间。编写最小示例时需要同时引用对应的消息与 handler 命名空间。
|
||||||
|
|
||||||
### Dispatcher(请求分发器)
|
### Dispatcher(请求分发器)
|
||||||
|
|
||||||
架构上下文会负责将命令、查询和通知路由到对应的处理器:
|
架构上下文会负责将命令、查询和通知路由到对应的处理器:
|
||||||
|
|||||||
@ -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
|
||||||
@ -62,6 +68,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" />
|
||||||
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试代码
|
// 测试代码
|
||||||
|
|||||||
@ -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 字段和便捷的日志方法。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user