Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ Follow them strictly.

```text
GFramework (meta package) ─→ Core + Game
GFramework.Cqrs ─→ Cqrs.Abstractions, Core.Abstractions
GFramework.Core ─→ Core.Abstractions
GFramework.Game ─→ Game.Abstractions, Core, Core.Abstractions
GFramework.Godot ─→ Core, Game, Core.Abstractions, Game.Abstractions
GFramework.Ecs.Arch ─→ Ecs.Arch.Abstractions, Core, Core.Abstractions
GFramework.SourceGenerators ─→ SourceGenerators.Common, SourceGenerators.Abstractions
GFramework.Core.SourceGenerators ─→ Core.SourceGenerators.Abstractions, SourceGenerators.Common
GFramework.Game.SourceGenerators ─→ SourceGenerators.Common
GFramework.Godot.SourceGenerators ─→ Godot.SourceGenerators.Abstractions, SourceGenerators.Common
GFramework.Cqrs.SourceGenerators ─→ SourceGenerators.Common
```

- **Abstractions projects** (`netstandard2.1`): 只包含接口和契约定义,不承载运行时实现逻辑。
- **Core / Game / Ecs.Arch** (`net8.0;net9.0;net10.0`): 平台无关的核心实现层。
- **Godot**: Godot 引擎集成层,负责与节点、场景和引擎生命周期对接。
- **SourceGenerators** (`netstandard2.1`): Roslyn 增量源码生成器及其公共基础设施。
- **SourceGenerators family** (`netstandard2.0`/`netstandard2.1`): 按 Core / Game / Godot / Cqrs 拆分的 Roslyn
增量源码生成器,以及共享的 abstractions/common 基础设施。

## Architecture Pattern

Expand Down Expand Up @@ -114,10 +119,12 @@ Architecture 负责统一生命周期编排,核心阶段包括:
仓库以“抽象层 + 实现层 + 集成层 + 生成器层”的方式组织:

- `GFramework.Core.Abstractions` / `GFramework.Game.Abstractions`: 约束接口和公共契约。
- `GFramework.Cqrs.Abstractions` / `GFramework.Cqrs`: 提供 CQRS 契约、runtime 与 handler 注册基础设施。
- `GFramework.Core` / `GFramework.Game`: 提供平台无关实现。
- `GFramework.Godot`: 提供与 Godot 运行时集成的适配实现。
- `GFramework.Ecs.Arch`: 提供 ECS Architecture 相关扩展。
- `GFramework.SourceGenerators` 及相关 Abstractions/Common: 提供代码生成能力。
- `GFramework.Core.SourceGenerators` / `GFramework.Game.SourceGenerators` / `GFramework.Godot.SourceGenerators` /
`GFramework.Cqrs.SourceGenerators` 与相关 Abstractions/Common: 提供代码生成能力。

这种结构的核心设计目标是让抽象稳定、实现可替换、引擎集成隔离、生成器能力可独立演进。

Expand Down
93 changes: 34 additions & 59 deletions GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@
namespace GFramework.Cqrs.Tests.Cqrs;

/// <summary>
/// 验证 CQRS dispatcher 会缓存热路径中的服务类型与调用委托
/// 验证 CQRS dispatcher 会缓存热路径中的 dispatch binding
/// </summary>
[TestFixture]
internal sealed class CqrsDispatcherCacheTests
{
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;

/// <summary>
/// 初始化测试上下文。
/// </summary>
Expand Down Expand Up @@ -45,40 +42,31 @@ public void TearDown()
_container = null;
}

private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;

/// <summary>
/// 验证相同消息类型重复分发时,不会重复扩张服务类型与调用委托缓存
/// 验证相同消息类型重复分发时,不会重复扩张 dispatch binding 缓存
/// </summary>
[Test]
public async Task Dispatcher_Should_Cache_Service_Types_After_First_Dispatch()
public async Task Dispatcher_Should_Cache_Dispatch_Bindings_After_First_Dispatch()
{
var notificationServiceTypes = GetCacheField("NotificationHandlerServiceTypes");
var requestServiceTypes = GetCacheField("RequestServiceTypes");
var streamServiceTypes = GetCacheField("StreamHandlerServiceTypes");
var requestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers");
var requestPipelineInvokers = GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers");
var notificationInvokers = GetCacheField("NotificationInvokers");
var streamInvokers = GetCacheField("StreamInvokers");

var notificationBefore = notificationServiceTypes.Count;
var requestBefore = requestServiceTypes.Count;
var streamBefore = streamServiceTypes.Count;
var requestInvokersBefore = requestInvokers.Count;
var requestPipelineInvokersBefore = requestPipelineInvokers.Count;
var notificationInvokersBefore = notificationInvokers.Count;
var streamInvokersBefore = streamInvokers.Count;
var notificationBindings = GetCacheField("NotificationDispatchBindings");
var requestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings");
var streamBindings = GetCacheField("StreamDispatchBindings");

var notificationBefore = notificationBindings.Count;
var requestBefore = requestBindings.Count;
var streamBefore = streamBindings.Count;

await _context!.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
await _context.PublishAsync(new DispatcherCacheNotification());
await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest()));

var notificationAfterFirstDispatch = notificationServiceTypes.Count;
var requestAfterFirstDispatch = requestServiceTypes.Count;
var streamAfterFirstDispatch = streamServiceTypes.Count;
var requestInvokersAfterFirstDispatch = requestInvokers.Count;
var requestPipelineInvokersAfterFirstDispatch = requestPipelineInvokers.Count;
var notificationInvokersAfterFirstDispatch = notificationInvokers.Count;
var streamInvokersAfterFirstDispatch = streamInvokers.Count;
var notificationAfterFirstDispatch = notificationBindings.Count;
var requestAfterFirstDispatch = requestBindings.Count;
var streamAfterFirstDispatch = streamBindings.Count;

await _context.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
Expand All @@ -90,38 +78,30 @@ public async Task Dispatcher_Should_Cache_Service_Types_After_First_Dispatch()
Assert.That(notificationAfterFirstDispatch, Is.EqualTo(notificationBefore + 1));
Assert.That(requestAfterFirstDispatch, Is.EqualTo(requestBefore + 2));
Assert.That(streamAfterFirstDispatch, Is.EqualTo(streamBefore + 1));
Assert.That(requestInvokersAfterFirstDispatch, Is.EqualTo(requestInvokersBefore + 1));
Assert.That(requestPipelineInvokersAfterFirstDispatch, Is.EqualTo(requestPipelineInvokersBefore + 1));
Assert.That(notificationInvokersAfterFirstDispatch, Is.EqualTo(notificationInvokersBefore + 1));
Assert.That(streamInvokersAfterFirstDispatch, Is.EqualTo(streamInvokersBefore + 1));

Assert.That(notificationServiceTypes.Count, Is.EqualTo(notificationAfterFirstDispatch));
Assert.That(requestServiceTypes.Count, Is.EqualTo(requestAfterFirstDispatch));
Assert.That(streamServiceTypes.Count, Is.EqualTo(streamAfterFirstDispatch));
Assert.That(requestInvokers.Count, Is.EqualTo(requestInvokersAfterFirstDispatch));
Assert.That(requestPipelineInvokers.Count, Is.EqualTo(requestPipelineInvokersAfterFirstDispatch));
Assert.That(notificationInvokers.Count, Is.EqualTo(notificationInvokersAfterFirstDispatch));
Assert.That(streamInvokers.Count, Is.EqualTo(streamInvokersAfterFirstDispatch));

Assert.That(notificationBindings.Count, Is.EqualTo(notificationAfterFirstDispatch));
Assert.That(requestBindings.Count, Is.EqualTo(requestAfterFirstDispatch));
Assert.That(streamBindings.Count, Is.EqualTo(streamAfterFirstDispatch));
});
}

/// <summary>
/// 验证 request 调用委托会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。
/// 验证 request dispatch binding 会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。
/// </summary>
[Test]
public async Task Dispatcher_Should_Cache_Request_Invokers_Per_Response_Type()
public async Task Dispatcher_Should_Cache_Request_Dispatch_Bindings_Per_Response_Type()
{
var intRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers");
var stringRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers");
var intRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings");
var stringRequestBindings = GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings");

var intBefore = intRequestInvokers.Count;
var stringBefore = stringRequestInvokers.Count;
var intBefore = intRequestBindings.Count;
var stringBefore = stringRequestBindings.Count;

await _context!.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherStringCacheRequest());

var intAfterFirstDispatch = intRequestInvokers.Count;
var stringAfterFirstDispatch = stringRequestInvokers.Count;
var intAfterFirstDispatch = intRequestBindings.Count;
var stringAfterFirstDispatch = stringRequestBindings.Count;

await _context.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherStringCacheRequest());
Expand All @@ -130,8 +110,8 @@ public async Task Dispatcher_Should_Cache_Request_Invokers_Per_Response_Type()
{
Assert.That(intAfterFirstDispatch, Is.EqualTo(intBefore + 1));
Assert.That(stringAfterFirstDispatch, Is.EqualTo(stringBefore + 1));
Assert.That(intRequestInvokers.Count, Is.EqualTo(intAfterFirstDispatch));
Assert.That(stringRequestInvokers.Count, Is.EqualTo(stringAfterFirstDispatch));
Assert.That(intRequestBindings.Count, Is.EqualTo(intAfterFirstDispatch));
Assert.That(stringRequestBindings.Count, Is.EqualTo(stringAfterFirstDispatch));
});
}

Expand All @@ -157,15 +137,10 @@ private static IDictionary GetCacheField(string fieldName)
/// </summary>
private static void ClearDispatcherCaches()
{
GetCacheField("NotificationHandlerServiceTypes").Clear();
GetCacheField("RequestServiceTypes").Clear();
GetCacheField("StreamHandlerServiceTypes").Clear();
GetCacheField("NotificationInvokers").Clear();
GetCacheField("StreamInvokers").Clear();
GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers").Clear();
GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers").Clear();
GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers").Clear();
GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(string), "Invokers").Clear();
GetCacheField("NotificationDispatchBindings").Clear();
GetCacheField("StreamDispatchBindings").Clear();
GetGenericCacheField("RequestDispatchBindingCache`1", typeof(int), "Bindings").Clear();
GetGenericCacheField("RequestDispatchBindingCache`1", typeof(string), "Bindings").Clear();
}

/// <summary>
Expand Down
91 changes: 91 additions & 0 deletions GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,97 @@ public void RegisterHandlers_Should_Use_Direct_Fallback_Types_Without_GetType_Or
Times.Never);
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
}

/// <summary>
/// 验证同一程序集对象重复接入多个容器时,会复用已解析的 registry / fallback 元数据,
/// 而不是重复读取程序集级 attribute 或重复执行 type-name lookup。
/// </summary>
[Test]
public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
]);
generatedAssembly
.Setup(static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false))
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);

var firstContainer = new MicrosoftDiContainer();
var secondContainer = new MicrosoftDiContainer();

CqrsTestRuntime.RegisterHandlers(firstContainer, generatedAssembly.Object);
CqrsTestRuntime.RegisterHandlers(secondContainer, generatedAssembly.Object);

generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Once);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// <summary>
/// 验证同一程序集对象在未命中 generated registry 时,会复用首次扫描得到的可加载类型列表,
/// 而不是为每个容器重复执行整程序集 <c>GetTypes()</c>。
/// </summary>
[Test]
public void RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers()
{
var reflectionTypeLoadException = new ReflectionTypeLoadException(
[typeof(AlphaDeterministicNotificationHandler), null],
[new TypeLoadException("Cached loadable-type probe.")]);
var partiallyLoadableAssembly = new Mock<Assembly>();
partiallyLoadableAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedLoadableTypesAssembly, Version=1.0.0.0");
partiallyLoadableAssembly
.Setup(static assembly => assembly.GetTypes())
.Throws(reflectionTypeLoadException);

var firstContainer = new MicrosoftDiContainer();
var secondContainer = new MicrosoftDiContainer();

CqrsTestRuntime.RegisterHandlers(firstContainer, partiallyLoadableAssembly.Object);
CqrsTestRuntime.RegisterHandlers(secondContainer, partiallyLoadableAssembly.Object);
firstContainer.Freeze();
secondContainer.Freeze();

Assert.Multiple(() =>
{
Assert.That(
firstContainer.GetAll<INotificationHandler<DeterministicOrderNotification>>()
.Select(static handler => handler.GetType())
.ToArray(),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
Assert.That(
secondContainer.GetAll<INotificationHandler<DeterministicOrderNotification>>()
.Select(static handler => handler.GetType())
.ToArray(),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
});

partiallyLoadableAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once);
}
}

/// <summary>
Expand Down
Loading
Loading