diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9bfcf89..defe6c71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,12 @@ jobs: --logger "trx;LogFileName=sg-$RANDOM.trx" \ --results-directory TestResults & + dotnet test GFramework.Cqrs.Tests \ + -c Release \ + --no-build \ + --logger "trx;LogFileName=cqrs-$RANDOM.trx" \ + --results-directory TestResults & + dotnet test GFramework.Ecs.Arch.Tests \ -c Release \ --no-build \ diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index b9b5dc9a..0eb6a43f 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -1,11 +1,11 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Abstractions.Architectures; @@ -131,7 +131,7 @@ public interface IArchitectureContext /// /// 这是迁移后的推荐命令入口。无返回值命令应实现 IRequest<Unit>,并优先通过 调用。 /// - TResponse SendCommand(Cqrs.Command.ICommand command); + TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command); /// @@ -147,7 +147,8 @@ public interface IArchitectureContext /// 要发送的 CQRS 命令。 /// 取消令牌。 /// 包含命令执行结果的值任务。 - ValueTask SendCommandAsync(Cqrs.Command.ICommand command, + ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default); @@ -176,7 +177,7 @@ public interface IArchitectureContext /// /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse>。 /// - TResponse SendQuery(Cqrs.Query.IQuery query); + TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query); /// /// 异步发送一个旧版查询请求。 @@ -193,7 +194,7 @@ public interface IArchitectureContext /// 要发送的 CQRS 查询。 /// 取消令牌。 /// 包含查询结果的值任务。 - ValueTask SendQueryAsync(Cqrs.Query.IQuery query, + ValueTask SendQueryAsync(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default); /// diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs deleted file mode 100644 index ba04331e..00000000 --- a/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace GFramework.Core.Abstractions.Cqrs.Command; - -/// -/// 表示一个 CQRS 命令。 -/// 命令通常用于修改系统状态。 -/// -/// 命令响应类型。 -public interface ICommand : IRequest -{ -} - -/// -/// 表示一个无显式返回值的 CQRS 命令。 -/// -public interface ICommand : ICommand -{ -} - -/// -/// 表示一个流式 CQRS 命令。 -/// -/// 流式响应元素类型。 -public interface IStreamCommand : IStreamRequest -{ -} diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs new file mode 100644 index 00000000..d9efcb07 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs @@ -0,0 +1,52 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义架构上下文使用的 CQRS runtime seam。 +/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦, +/// 使 不再直接依赖某个固定的 runtime 类型。 +/// +public interface ICqrsRuntime +{ + /// + /// 发送请求并返回响应。 + /// + /// 响应类型。 + /// 当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。 + /// 要分发的请求。 + /// 取消令牌。 + /// 请求响应。 + ValueTask SendAsync( + IArchitectureContext context, + IRequest request, + CancellationToken cancellationToken = default); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 要发布的通知。 + /// 取消令牌。 + /// 表示通知分发完成的值任务。 + ValueTask PublishAsync( + IArchitectureContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification; + + /// + /// 创建流式请求的异步响应序列。 + /// + /// 流元素类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 流式请求。 + /// 取消令牌。 + /// 按需生成的异步响应序列。 + IAsyncEnumerable CreateStream( + IArchitectureContext context, + IStreamRequest request, + CancellationToken cancellationToken = default); +} diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs deleted file mode 100644 index cbb1586e..00000000 --- a/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace GFramework.Core.Abstractions.Cqrs.Query; - -/// -/// 表示一个 CQRS 查询。 -/// 查询用于读取数据,不应产生副作用。 -/// -/// 查询响应类型。 -public interface IQuery : IRequest -{ -} - -/// -/// 表示一个流式 CQRS 查询。 -/// -/// 流式响应元素类型。 -public interface IStreamQuery : IStreamRequest -{ -} diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index 84d53f63..7a70b332 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -17,6 +17,9 @@ + + + all diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs index 04ee0571..67255b41 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -3,6 +3,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -12,8 +13,6 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public sealed class ArchitectureAdditionalCqrsHandlersTests { - private ILoggerFactoryProvider? _previousLoggerFactoryProvider; - /// /// 初始化日志工厂和共享测试状态。 /// @@ -39,6 +38,8 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests "LoggerFactoryResolver.Provider should be captured during setup."); } + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + /// /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 /// @@ -197,4 +198,4 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl }); return handler.Object; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 00684cb9..87978cd2 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -1,8 +1,10 @@ using System.Reflection; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Environment; +using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; @@ -14,6 +16,7 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -73,13 +76,14 @@ public class ArchitectureContextTests _context = new ArchitectureContext(_container); } - private ArchitectureContext? _context; - private MicrosoftDiContainer? _container; - private EventBus? _eventBus; - private CommandExecutor? _commandBus; - private QueryExecutor? _queryBus; private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; /// /// 测试构造函数在所有参数都有效时不应抛出异常 @@ -298,6 +302,76 @@ public class ArchitectureContextTests Assert.That(environment, Is.Not.Null); Assert.That(environment, Is.InstanceOf()); } + + /// + /// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。 + /// + [Test] + public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() + { + const int workerCount = 8; + var workerStartupTimeout = TimeSpan.FromSeconds(5); + var firstResolutionTimeout = TimeSpan.FromSeconds(5); + using var startGate = new ManualResetEventSlim(false); + using var allowResolutionToComplete = new ManualResetEventSlim(false); + using var workersReady = new CountdownEvent(workerCount); + var resolutionCallCount = 0; + var runtime = new Mock(MockBehavior.Strict); + var container = new Mock(MockBehavior.Strict); + + runtime.Setup(mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask(42)); + + container.Setup(mockContainer => mockContainer.Get()) + .Returns(() => + { + Interlocked.Increment(ref resolutionCallCount); + allowResolutionToComplete.Wait(); + return runtime.Object; + }); + + var context = new ArchitectureContext(container.Object); + var requests = Enumerable.Range(0, workerCount) + .Select(_ => Task.Run(async () => + { + workersReady.Signal(); + startGate.Wait(); + return await context.SendRequestAsync(new TestCqrsRequest()); + })) + .ToArray(); + + Assert.That( + workersReady.Wait(workerStartupTimeout), + Is.True, + "Expected all workers to be ready before releasing start gate."); + startGate.Set(); + + Assert.That( + SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), + Is.True, + "Expected at least one CQRS runtime resolution attempt."); + + allowResolutionToComplete.Set(); + + var responses = await Task.WhenAll(requests); + + Assert.That(responses, Has.All.EqualTo(42)); + Assert.That(resolutionCallCount, Is.EqualTo(1)); + container.Verify(mockContainer => mockContainer.Get(), Times.Once); + runtime.Verify( + mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Exactly(requests.Length)); + } + + private sealed class TestCqrsRequest : IRequest + { + } } #region Test Classes @@ -442,4 +516,4 @@ public class TestEventV2 public int Data { get; init; } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index ade4a1a7..cfe0db79 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -2,7 +2,7 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; using GFramework.Core.Logging; -using GfCqrs = GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Architectures; @@ -151,14 +151,14 @@ public class ArchitectureModulesBehaviorTests /// /// 用于验证管道行为注册是否生效的测试请求。 /// -public sealed class ModuleBehaviorRequest : GfCqrs.IRequest +public sealed class ModuleBehaviorRequest : IRequest { } /// /// 处理测试请求的处理器。 /// -public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler +public sealed class ModuleBehaviorRequestHandler : IRequestHandler { /// /// 返回固定结果,便于聚焦验证管道行为是否执行。 @@ -177,8 +177,8 @@ public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler /// 请求类型。 /// 响应类型。 -public sealed class TrackingPipelineBehavior : GfCqrs.IPipelineBehavior - where TRequest : GfCqrs.IRequest +public sealed class TrackingPipelineBehavior : IPipelineBehavior + where TRequest : IRequest { /// /// 获取当前测试进程中该请求类型对应的行为触发次数。 @@ -193,7 +193,7 @@ public sealed class TrackingPipelineBehavior : GfCqrs.IPipe /// 取消令牌。 /// 下游处理器的响应结果。 public async ValueTask Handle( - TRequest message, GfCqrs.MessageHandlerDelegate next, + TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) { InvocationCount++; diff --git a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs index 94b749b6..52c2ecbd 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs @@ -1,6 +1,5 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -14,6 +13,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Architectures; @@ -34,10 +34,6 @@ namespace GFramework.Core.Tests.Architectures; [TestFixture] public class ArchitectureServicesTests { - private TestArchitectureContextV3? _context; - - private ArchitectureServices? _services; - [SetUp] public void SetUp() { @@ -45,6 +41,10 @@ public class ArchitectureServicesTests _context = new TestArchitectureContextV3(); } + private TestArchitectureContextV3? _context; + + private ArchitectureServices? _services; + private void RegisterBuiltInServices() { _services!.ModuleManager.RegisterBuiltInModules(_services.Container); @@ -359,24 +359,56 @@ public class TestArchitectureContextV3 : IArchitectureContext throw new NotImplementedException(); } - public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, + /// + /// 测试桩:异步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令响应任务。 + /// 该测试桩未实现此成员。 + public ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) + /// + /// 测试桩:同步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 命令响应。 + /// 该测试桩未实现此成员。 + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, + /// + /// 测试桩:异步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 取消令牌。 + /// 查询结果任务。 + /// 该测试桩未实现此成员。 + public ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) + /// + /// 测试桩:同步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 查询结果。 + /// 该测试桩未实现此成员。 + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index 28e3cc97..9b990e78 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -1,6 +1,5 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; @@ -14,6 +13,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Tests.Architectures; @@ -428,7 +428,8 @@ public class TestArchitectureContext : IArchitectureContext /// 取消令牌。 /// 命令响应任务。 /// 该测试桩未实现此成员。 - public ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, + public ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -441,7 +442,7 @@ public class TestArchitectureContext : IArchitectureContext /// 要发送的命令。 /// 命令响应。 /// 该测试桩未实现此成员。 - public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } @@ -454,7 +455,8 @@ public class TestArchitectureContext : IArchitectureContext /// 取消令牌。 /// 查询结果任务。 /// 该测试桩未实现此成员。 - public ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, + public ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -467,7 +469,7 @@ public class TestArchitectureContext : IArchitectureContext /// 要发送的查询。 /// 查询结果。 /// 该测试桩未实现此成员。 - public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } diff --git a/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs b/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs index e7a0611e..2cc2ec13 100644 --- a/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs +++ b/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs @@ -1,5 +1,4 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Architectures; using GFramework.Core.Command; @@ -7,6 +6,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Tests.Command; @@ -396,4 +396,4 @@ public sealed class TestAsyncCommandWithResultChildV3 : AbstractAsyncCommand +/// 为容器层测试提供可扫描的最小通知夹具。 +/// +internal sealed record DeterministicOrderNotification : INotification; + +/// +/// 供容器注册测试验证程序集扫描结果的通知处理器。 +/// +internal sealed class DeterministicOrderNotificationHandler : INotificationHandler +{ + /// + /// 无副作用地消费通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs index 2e3bdbca..b25806f0 100644 --- a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -35,16 +35,16 @@ public class MediatorCompatibilityDeprecationTests { AssertLegacyType( typeof(ContextAwareMediatorExtensions), - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead."); + "Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorCommandExtensions), - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead."); + "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead."); AssertLegacyType( typeof(ContextAwareMediatorQueryExtensions), - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead."); + "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead."); AssertLegacyType( typeof(MediatorCoroutineExtensions), - "Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead."); + "Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead."); } /// diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs deleted file mode 100644 index e9664925..00000000 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Reflection; -using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Architectures; -using GFramework.Core.Ioc; -using GFramework.Core.Logging; - -namespace GFramework.Core.Tests; - -/// -/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 -/// -/// -/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法, -/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。 -/// -internal static class CqrsTestRuntime -{ - private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly - .GetType( - "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", - throwOnError: true)! - ?? throw new InvalidOperationException( - "Failed to locate CqrsHandlerRegistrar type."); - - private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType - .GetMethod( - "RegisterHandlers", - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static, - binder: null, - [ - typeof(IIocContainer), - typeof(IEnumerable), - typeof(ILogger) - ], - modifiers: null) - ?? throw new InvalidOperationException( - "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); - - /// - /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 - /// - /// 承载处理器映射的测试容器。 - /// 要扫描的程序集集合。 - internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) - { - ArgumentNullException.ThrowIfNull(container); - ArgumentNullException.ThrowIfNull(assemblies); - - var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); - RegisterHandlersMethod.Invoke( - null, - [container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]); - } -} diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index 97663fe0..391b2924 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index fe9b7de1..96b8bb1a 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -16,6 +16,7 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; +global using GFramework.Tests.Common; global using NUnit.Framework; global using NUnit.Compatibility; global using GFramework.Core.Systems; diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index b4e5af67..0e03059e 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -5,6 +5,7 @@ using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; using GFramework.Core.Tests.Systems; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Tests.Ioc; @@ -29,6 +30,8 @@ public class MicrosoftDiContainerTests BindingFlags.NonPublic | BindingFlags.Instance); loggerField?.SetValue(_container, LoggerFactoryResolver.Provider.CreateLogger(nameof(MicrosoftDiContainer))); + + CqrsTestRuntime.RegisterInfrastructure(_container); } private MicrosoftDiContainer _container = null!; @@ -149,6 +152,21 @@ public class MicrosoftDiContainerTests Assert.That(result, Is.SameAs(instance)); } + /// + /// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。 + /// + [Test] + public void RegisterHandlers_Should_Not_Duplicate_Cqrs_Infrastructure_When_It_Is_Already_Registered() + { + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + + CqrsTestRuntime.RegisterHandlers(_container); + + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + } + /// /// 测试当没有实例时获取应返回 null 的功能 /// @@ -314,7 +332,7 @@ public class MicrosoftDiContainerTests [Test] public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State() { - var assembly = typeof(CqrsHandlerRegistrarTests).Assembly; + var assembly = typeof(DeterministicOrderNotification).Assembly; _container.RegisterCqrsHandlersFromAssembly(assembly); Assert.That( @@ -328,6 +346,8 @@ public class MicrosoftDiContainerTests descriptor.ServiceType == typeof(INotificationHandler)), Is.False); + // Clear 会移除测试手工补齐的 CQRS seam,需要先恢复基础设施再验证程序集去重状态是否已重置。 + CqrsTestRuntime.RegisterInfrastructure(_container); _container.RegisterCqrsHandlersFromAssembly(assembly); Assert.That( diff --git a/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs b/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs index 7a645ac1..bf6397b9 100644 --- a/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs +++ b/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs @@ -1,4 +1,3 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Architectures; @@ -7,6 +6,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -411,4 +411,4 @@ public sealed class TestAsyncQueryResultV2 /// 获取或设置双倍值 /// public int DoubleValue { get; init; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs index 6491946d..b23cc8f9 100644 --- a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs @@ -1,5 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -292,4 +292,4 @@ public sealed class TestAsyncQueryResult /// 获取或设置双倍值 /// public int DoubleValue { get; init; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Query/QueryExecutorTests.cs b/GFramework.Core.Tests/Query/QueryExecutorTests.cs index f0305b4b..a9cde117 100644 --- a/GFramework.Core.Tests/Query/QueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/QueryExecutorTests.cs @@ -1,5 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -121,4 +121,4 @@ public sealed class TestStringQuery : AbstractQuery { return $"Result: {input.Value * 2}"; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 77c04fcf..9b6d7dc2 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -2,18 +2,14 @@ using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using GFramework.Core.Cqrs.Internal; -using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Architectures; @@ -21,19 +17,48 @@ namespace GFramework.Core.Architectures; /// /// 架构上下文类,提供对系统、模型、工具等组件的访问以及命令、查询、事件的执行管理 /// -public class ArchitectureContext(IIocContainer container) : IArchitectureContext +public class ArchitectureContext : IArchitectureContext { - private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly IIocContainer _container; + private readonly Lazy _cqrsRuntime; private readonly ConcurrentDictionary _serviceCache = new(); - private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContext)); - private CqrsDispatcher? _cqrsDispatcher; + + /// + /// 初始化新的架构上下文,并绑定其依赖容器。 + /// + /// + /// 当前架构使用的 IOC 容器。 + /// CQRS runtime 与其他框架服务会通过该容器延迟解析,以避免在上下文构造阶段强制拉起整条运行时链路。 + /// + /// + public ArchitectureContext(IIocContainer container) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _cqrsRuntime = new Lazy( + ResolveCqrsRuntime, + LazyThreadSafetyMode.ExecutionAndPublication); + } #region CQRS Integration /// - /// 获取 CQRS 运行时分发器(延迟初始化)。 + /// 获取 CQRS runtime seam。 /// - private CqrsDispatcher CqrsDispatcher => _cqrsDispatcher ??= new CqrsDispatcher(_container, this, _logger); + /// + /// 该实例会在首次访问时从容器解析,并通过 保证并发场景下只执行一次初始化, + /// 避免多个请求线程重复触发同一个 runtime 的容器解析。 + /// + private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value; + + /// + /// 从容器解析当前架构上下文依赖的 CQRS runtime。 + /// + /// 已注册的 CQRS runtime 实例。 + /// 容器中未注册 + private ICqrsRuntime ResolveCqrsRuntime() + { + return _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); + } /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 @@ -73,7 +98,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await CqrsDispatcher.SendAsync(request, cancellationToken); + return await CqrsRuntime.SendAsync(this, request, cancellationToken); } /// @@ -100,7 +125,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext where TNotification : INotification { ArgumentNullException.ThrowIfNull(notification); - await CqrsDispatcher.PublishAsync(notification, cancellationToken); + await CqrsRuntime.PublishAsync(this, notification, cancellationToken); } /// @@ -115,7 +140,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return CqrsDispatcher.CreateStream(request, cancellationToken); + return CqrsRuntime.CreateStream(this, request, cancellationToken); } /// @@ -151,7 +176,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询结果类型 /// 要发送的查询 /// 查询结果 - public TResult SendQuery(Abstractions.Query.IQuery query) + public TResult SendQuery(IQuery query) { if (query == null) throw new ArgumentNullException(nameof(query)); var queryBus = GetOrCache(); @@ -165,7 +190,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(GFramework.Core.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -191,7 +216,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(GFramework.Core.Abstractions.Cqrs.Query.IQuery query, + public async ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -327,7 +353,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的命令对象 /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask - public async ValueTask SendCommandAsync(GFramework.Core.Abstractions.Cqrs.Command.ICommand command, + public async ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -366,7 +393,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(GFramework.Core.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } @@ -388,7 +415,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令执行结果类型 /// 要发送的命令 /// 命令执行结果 - public TResult SendCommand(Abstractions.Command.ICommand command) + public TResult SendCommand(ICommand command) { ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs index 491bbd8e..d97ab90b 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -26,4 +26,4 @@ public abstract class AbstractAsyncCommand(TInput input) : ContextAwareB /// 命令输入参数 /// 表示异步操作的任务 protected abstract Task OnExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs index 47390192..ae19458e 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -27,4 +27,4 @@ public abstract class AbstractAsyncCommand(TInput input) : Cont /// 命令输入参数 /// 表示异步操作且包含结果的任务 protected abstract Task OnExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractCommandWithInput.cs b/GFramework.Core/Command/AbstractCommandWithInput.cs index 7512c760..c3ebbb03 100644 --- a/GFramework.Core/Command/AbstractCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractCommandWithInput.cs @@ -1,6 +1,6 @@ -using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Command; @@ -9,13 +9,13 @@ namespace GFramework.Core.Command; /// /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand +public abstract class AbstractCommand(TInput input) : ContextAwareBase, ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法 /// - void GFramework.Core.Abstractions.Command.ICommand.Execute() + void ICommand.Execute() { OnExecute(input); } diff --git a/GFramework.Core/Command/AbstractCommandWithResult.cs b/GFramework.Core/Command/AbstractCommandWithResult.cs index 7ecc9522..ca40957a 100644 --- a/GFramework.Core/Command/AbstractCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractCommandWithResult.cs @@ -1,6 +1,5 @@ -using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -10,14 +9,15 @@ namespace GFramework.Core.Command; /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行后返回的结果类型 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand +public abstract class AbstractCommand(TInput input) + : ContextAwareBase, Abstractions.Command.ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法 /// /// 命令执行后的结果 - TResult GFramework.Core.Abstractions.Command.ICommand.Execute() + TResult Abstractions.Command.ICommand.Execute() { return OnExecute(input); } diff --git a/GFramework.Core/Command/EmptyCommandInput.cs b/GFramework.Core/Command/EmptyCommandInput.cs index 4d08f4e7..540c158e 100644 --- a/GFramework.Core/Command/EmptyCommandInput.cs +++ b/GFramework.Core/Command/EmptyCommandInput.cs @@ -1,4 +1,4 @@ -using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -9,4 +9,4 @@ namespace GFramework.Core.Command; /// 该类实现了ICommandInput接口,作为命令模式中的输入参数载体 /// 通常用于不需要额外输入参数的简单命令操作 /// -public sealed class EmptyCommandInput : ICommandInput; \ No newline at end of file +public sealed class EmptyCommandInput : ICommandInput; diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs index 98667656..ec2b55a0 100644 --- a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -1,10 +1,9 @@ using System.Runtime.ExceptionServices; using GFramework.Core.Abstractions.Coroutine; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Coroutine.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index f955f175..5086e392 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -13,20 +13,19 @@ using System.ComponentModel; using GFramework.Core.Abstractions.Coroutine; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Coroutine.Extensions; /// /// 提供 CQRS 命令与协程集成的扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")] public static class MediatorCoroutineExtensions { /// diff --git a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs index 4aaf797a..7230f53d 100644 --- a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs @@ -12,9 +12,9 @@ // limitations under the License. using System.Diagnostics; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Behaviors; diff --git a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs b/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs index 7fc266d0..35ab2978 100644 --- a/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs +++ b/GFramework.Core/Cqrs/Behaviors/PerformanceBehavior.cs @@ -12,9 +12,9 @@ // limitations under the License. using System.Diagnostics; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Behaviors; diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs index 528de106..d7ebb117 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs index 847563c4..223a9cc5 100644 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; diff --git a/GFramework.Core/Cqrs/Command/CommandBase.cs b/GFramework.Core/Cqrs/Command/CommandBase.cs index 78fa134e..d8232608 100644 --- a/GFramework.Core/Cqrs/Command/CommandBase.cs +++ b/GFramework.Core/Cqrs/Command/CommandBase.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Cqrs.Command; diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs index 69f6794d..0cae34c4 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs @@ -5,6 +5,7 @@ using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Internal; @@ -14,34 +15,76 @@ namespace GFramework.Core.Cqrs.Internal; /// internal sealed class CqrsDispatcher( IIocContainer container, - IArchitectureContext context, - ILogger logger) + ILogger logger) : ICqrsRuntime { - private delegate ValueTask RequestInvoker(object handler, object request, CancellationToken cancellationToken); - private delegate ValueTask RequestPipelineInvoker( - object handler, - IReadOnlyList behaviors, - object request, - CancellationToken cancellationToken); - private delegate ValueTask NotificationInvoker(object handler, object notification, CancellationToken cancellationToken); - private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + // 进程级缓存:按请求/响应类型缓存直接处理器调用委托,避免热路径重复反射。 + // 线程安全依赖 ConcurrentDictionary;缓存与进程同寿命,默认假设请求类型集合有限且稳定。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> + RequestInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> RequestInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> RequestPipelineInvokers = new(); + // 进程级缓存:缓存带 pipeline 的请求调用委托,减少每次分发时的反射与表达式重建开销。 + // 若后续引入动态生成请求类型,需要重新评估该缓存的增长边界。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> + RequestPipelineInvokers = new(); + + // 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。 private static readonly ConcurrentDictionary NotificationInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new(); + + // 进程级缓存:缓存流式请求调用委托,避免每次创建流时重复解析反射签名。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = + new(); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 通知对象。 + /// 取消令牌。 + public async ValueTask PublishAsync( + IArchitectureContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(notification); + + var notificationType = notification.GetType(); + var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); + var handlers = container.GetAll(handlerType); + + if (handlers.Count == 0) + { + logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); + return; + } + + var invoker = NotificationInvokers.GetOrAdd( + notificationType, + CreateNotificationInvoker); + + foreach (var handler in handlers) + { + PrepareHandler(handler, context); + await invoker(handler, notification, cancellationToken); + } + } /// /// 发送请求并返回结果。 /// /// 响应类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 /// 请求对象。 /// 取消令牌。 /// 请求响应。 public async ValueTask SendAsync( + IArchitectureContext context, IRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); @@ -50,12 +93,12 @@ internal sealed class CqrsDispatcher( ?? throw new InvalidOperationException( $"No CQRS request handler registered for {requestType.FullName}."); - PrepareHandler(handler); + PrepareHandler(handler, context); var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)); var behaviors = container.GetAll(behaviorType); foreach (var behavior in behaviors) - PrepareHandler(behavior); + PrepareHandler(behavior, context); if (behaviors.Count == 0) { @@ -75,51 +118,20 @@ internal sealed class CqrsDispatcher( return pipelineResult is null ? default! : (TResponse)pipelineResult; } - /// - /// 发布通知到所有已注册处理器。 - /// - /// 通知类型。 - /// 通知对象。 - /// 取消令牌。 - public async ValueTask PublishAsync( - TNotification notification, - CancellationToken cancellationToken = default) - where TNotification : INotification - { - ArgumentNullException.ThrowIfNull(notification); - - var notificationType = notification.GetType(); - var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); - var handlers = container.GetAll(handlerType); - - if (handlers.Count == 0) - { - logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); - return; - } - - var invoker = NotificationInvokers.GetOrAdd( - notificationType, - CreateNotificationInvoker); - - foreach (var handler in handlers) - { - PrepareHandler(handler); - await invoker(handler, notification, cancellationToken); - } - } - /// /// 创建流式请求并返回异步响应序列。 /// /// 响应元素类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 /// 流式请求对象。 /// 取消令牌。 /// 异步响应序列。 public IAsyncEnumerable CreateStream( + IArchitectureContext context, IStreamRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); @@ -128,7 +140,7 @@ internal sealed class CqrsDispatcher( ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); - PrepareHandler(handler); + PrepareHandler(handler, context); var invoker = StreamInvokers.GetOrAdd( (requestType, typeof(TResponse)), @@ -141,7 +153,8 @@ internal sealed class CqrsDispatcher( /// 为上下文感知处理器注入当前架构上下文。 /// /// 处理器实例。 - private void PrepareHandler(object handler) + /// 当前架构上下文。 + private static void PrepareHandler(object handler, IArchitectureContext context) { if (handler is IContextAware contextAware) contextAware.SetContext(context); @@ -260,4 +273,18 @@ internal sealed class CqrsDispatcher( var typedRequest = (TRequest)request; return typedHandler.Handle(typedRequest, cancellationToken); } + + private delegate ValueTask RequestInvoker(object handler, object request, + CancellationToken cancellationToken); + + private delegate ValueTask RequestPipelineInvoker( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken); + + private delegate ValueTask NotificationInvoker(object handler, object notification, + CancellationToken cancellationToken); + + private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); } diff --git a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs index 189ae8f0..65b1cbcf 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -2,6 +2,7 @@ using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Internal; diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs new file mode 100644 index 00000000..ddf0c06a --- /dev/null +++ b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Cqrs.Internal; + +/// +/// 默认的 CQRS 处理器注册器实现。 +/// 该适配器把容器公开的 handler 接入入口转发到现有的注册流水线, +/// 使容器主路径只依赖 抽象。 +/// +internal sealed class DefaultCqrsHandlerRegistrar(IIocContainer container, ILogger logger) : ICqrsHandlerRegistrar +{ + private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// 按当前 runtime 约定扫描并注册处理器程序集。 + /// + /// 要接入的程序集集合。 + public void RegisterHandlers(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + CqrsHandlerRegistrar.RegisterHandlers(_container, assemblies, _logger); + } +} diff --git a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs b/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs index 1b1157ab..da6f5281 100644 --- a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs +++ b/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Notification; diff --git a/GFramework.Core/Cqrs/Notification/NotificationBase.cs b/GFramework.Core/Cqrs/Notification/NotificationBase.cs index f04488b9..05db2de7 100644 --- a/GFramework.Core/Cqrs/Notification/NotificationBase.cs +++ b/GFramework.Core/Cqrs/Notification/NotificationBase.cs @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Notification; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Notification; namespace GFramework.Core.Cqrs.Notification; diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs index e9a3795f..85c86425 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; diff --git a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs index 015da1da..9695dc42 100644 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -12,8 +12,8 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Core/Cqrs/Query/QueryBase.cs index 6bccf549..759b8df1 100644 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ b/GFramework.Core/Cqrs/Query/QueryBase.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Cqrs.Query; diff --git a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs b/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs index 4ef6a270..f26c7cfa 100644 --- a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs +++ b/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; diff --git a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs b/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs index a15ed5d7..2cbf438d 100644 --- a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs +++ b/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs @@ -12,7 +12,7 @@ // limitations under the License. using GFramework.Core.Rule; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Cqrs.Request; diff --git a/GFramework.Core/Cqrs/Request/RequestBase.cs b/GFramework.Core/Cqrs/Request/RequestBase.cs index ce85784f..5ff18a04 100644 --- a/GFramework.Core/Cqrs/Request/RequestBase.cs +++ b/GFramework.Core/Cqrs/Request/RequestBase.cs @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Abstractions.Cqrs.Request; -using GFramework.Core.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Request; namespace GFramework.Core.Cqrs.Request; diff --git a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs index b71669ee..f99fae25 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -1,7 +1,7 @@ -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs index ab09e689..6db156f5 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsExtensions.cs @@ -1,7 +1,7 @@ -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 统一扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs index 9906bc3d..40ec0f7d 100644 --- a/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -1,7 +1,7 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Extensions; +namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 85f9776e..4ab0692b 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -1,18 +1,17 @@ using System.ComponentModel; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 命令扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorCommandExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index 68b130ce..f2294930 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -1,18 +1,17 @@ using System.ComponentModel; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 统一接口扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorExtensions { /// diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index d7fada4a..61b02cb9 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -1,18 +1,17 @@ using System.ComponentModel; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Extensions; /// /// 提供对 接口的 CQRS 查询扩展方法。 -/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 /// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete( - "Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] + "Use GFramework.Core.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")] public static class ContextAwareMediatorQueryExtensions { /// diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index c450b44c..f3e41eab 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -9,6 +9,7 @@ true + diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 712a41c1..dc14485e 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,13 +1,12 @@ using System.ComponentModel; using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; -using GFramework.Core.Cqrs.Internal; using GFramework.Core.Logging; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; namespace GFramework.Core.Ioc; @@ -424,7 +423,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) continue; } - CqrsHandlerRegistrar.RegisterHandlers(this, [assembly], _logger); + ResolveCqrsHandlerRegistrar().RegisterHandlers([assembly]); _registeredCqrsHandlerAssemblyKeys.Add(assemblyKey); } } @@ -456,6 +455,27 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) #region Get + /// + /// 获取当前容器中已注册的 CQRS 处理器注册器。 + /// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定, + /// 避免在容器未冻结前依赖完整的服务提供者构建流程。 + /// + /// 已注册的 CQRS 处理器注册器实例。 + /// 未找到可用的 CQRS 处理器注册器实例时抛出。 + private ICqrsHandlerRegistrar ResolveCqrsHandlerRegistrar() + { + var descriptor = GetServicesUnsafe.LastOrDefault(static service => + service.ServiceType == typeof(ICqrsHandlerRegistrar)); + + if (descriptor?.ImplementationInstance is ICqrsHandlerRegistrar registrar) + return registrar; + + const string errorMessage = + "ICqrsHandlerRegistrar not registered. Ensure the CQRS runtime module has been installed before registering handlers."; + _logger.Error(errorMessage); + throw new InvalidOperationException(errorMessage); + } + /// /// 获取指定泛型类型的服务实例 /// 返回第一个匹配的注册实例,如果不存在则返回null diff --git a/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs b/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs index 03c35071..03713712 100644 --- a/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs @@ -1,6 +1,6 @@ -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Query; +using GFramework.Core.Abstractions.Query; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -30,4 +30,4 @@ public abstract class AbstractAsyncQuery( /// 查询输入参数 /// 返回查询结果的异步任务 protected abstract Task OnDoAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Query/AbstractQueryWithResult.cs b/GFramework.Core/Query/AbstractQueryWithResult.cs index 2c87622a..ae099abe 100644 --- a/GFramework.Core/Query/AbstractQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractQueryWithResult.cs @@ -1,6 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Query; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -9,7 +8,8 @@ namespace GFramework.Core.Query; /// /// 查询输入参数的类型,必须实现IQueryInput接口 /// 查询结果的类型 -public abstract class AbstractQuery(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery +public abstract class AbstractQuery(TInput input) + : ContextAwareBase, Abstractions.Query.IQuery where TInput : IQueryInput { /// diff --git a/GFramework.Core/Query/EmptyQueryInput.cs b/GFramework.Core/Query/EmptyQueryInput.cs index 7d707189..a6a0cd67 100644 --- a/GFramework.Core/Query/EmptyQueryInput.cs +++ b/GFramework.Core/Query/EmptyQueryInput.cs @@ -1,4 +1,4 @@ -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -8,4 +8,4 @@ namespace GFramework.Core.Query; /// /// 该类实现了IQueryInput接口,作为占位符使用,适用于那些不需要额外输入参数的查询场景 /// -public sealed class EmptyQueryInput : IQueryInput; \ No newline at end of file +public sealed class EmptyQueryInput : IQueryInput; diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs new file mode 100644 index 00000000..5ca7a909 --- /dev/null +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -0,0 +1,62 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Cqrs.Internal; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Core.Services.Modules; + +/// +/// CQRS runtime 模块,用于把默认请求分发器与处理器注册器接入架构容器。 +/// 该模块在架构初始化早期完成注册,保证用户初始化阶段即可使用 CQRS 入口与 handler 自动接入能力。 +/// +public sealed class CqrsRuntimeModule : IServiceModule +{ + /// + /// 获取模块名称。 + /// + public string ModuleName => nameof(CqrsRuntimeModule); + + /// + /// 获取模块优先级。 + /// CQRS runtime 需要先于架构默认 handler 扫描路径可用,因此放在基础总线模块之后、用户初始化之前注册。 + /// + public int Priority => 15; + + /// + /// 获取模块启用状态,默认启用。 + /// + public bool IsEnabled => true; + + /// + /// 注册默认 CQRS runtime seam 实现。 + /// + /// 目标依赖注入容器。 + public void Register(IIocContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsDispatcher)); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(DefaultCqrsHandlerRegistrar)); + + container.Register(new CqrsDispatcher(container, dispatcherLogger)); + container.Register(new DefaultCqrsHandlerRegistrar(container, registrarLogger)); + } + + /// + /// 初始化模块。 + /// + public void Initialize() + { + } + + /// + /// 异步销毁模块。 + /// + /// 已完成的值任务。 + public ValueTask DestroyAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core/Services/ServiceModuleManager.cs b/GFramework.Core/Services/ServiceModuleManager.cs index d07c128b..a3965f4d 100644 --- a/GFramework.Core/Services/ServiceModuleManager.cs +++ b/GFramework.Core/Services/ServiceModuleManager.cs @@ -42,7 +42,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager /// /// 注册内置服务模块,并根据优先级排序后完成服务注册。 - /// 内置模块包括事件总线、命令执行器、查询执行器等核心模块。 + /// 内置模块包括事件总线、命令执行器、CQRS runtime、查询执行器等核心模块。 /// 同时注册通过 ArchitectureModuleRegistry 自动注册的外部模块。 /// /// IoC容器实例,用于模块服务注册。 @@ -57,6 +57,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager // 注册内置模块 RegisterModule(new EventBusModule()); RegisterModule(new CommandExecutorModule()); + RegisterModule(new CqrsRuntimeModule()); RegisterModule(new QueryExecutorModule()); RegisterModule(new AsyncQueryExecutorModule()); @@ -148,4 +149,4 @@ public sealed class ServiceModuleManager : IServiceModuleManager _builtInModulesRegistered = false; _logger.Info("All service modules destroyed"); } -} \ No newline at end of file +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs new file mode 100644 index 00000000..9c62f8f5 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs @@ -0,0 +1,13 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; + +/// +/// 表示一个 CQRS 命令。 +/// 命令通常用于修改系统状态。 +/// +/// 命令响应类型。 +public interface ICommand : IRequest; + +/// +/// 表示一个无显式返回值的 CQRS 命令。 +/// +public interface ICommand : ICommand; diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs similarity index 64% rename from GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs index 5ec607e5..9f5be0b6 100644 --- a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs @@ -1,7 +1,7 @@ -namespace GFramework.Core.Abstractions.Cqrs.Command; +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; /// /// 命令输入接口,定义命令模式中输入数据的契约 /// 该接口作为标记接口使用,不包含任何成员定义 /// -public interface ICommandInput : IInput; \ No newline at end of file +public interface ICommandInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs new file mode 100644 index 00000000..51323d19 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; + +/// +/// 表示一个流式 CQRS 命令。 +/// +/// 流式响应元素类型。 +public interface IStreamCommand : IStreamRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs new file mode 100644 index 00000000..39635d3f --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义 CQRS 处理器程序集接入的 runtime seam。 +/// 该抽象负责承接“生成注册器优先、反射扫描回退”的处理器注册流程, +/// 让容器与架构启动链不再直接依赖固定的注册实现类型。 +/// +public interface ICqrsHandlerRegistrar +{ + /// + /// 扫描并注册指定程序集集合中的 CQRS 处理器。 + /// + /// 要接入的程序集集合。 + void RegisterHandlers(IEnumerable assemblies); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs similarity index 91% rename from GFramework.Core.Abstractions/Cqrs/IInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IInput.cs index dfed5012..a8b6dbcb 100644 --- a/GFramework.Core.Abstractions/Cqrs/IInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示输入数据的标记接口。 /// 该接口用于标识各类CQRS模式中的输入参数类型。 /// -public interface IInput; \ No newline at end of file +public interface IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/INotification.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs similarity index 66% rename from GFramework.Core.Abstractions/Cqrs/INotification.cs rename to GFramework.Cqrs.Abstractions/Cqrs/INotification.cs index 9d69e28e..4a2dbb68 100644 --- a/GFramework.Core.Abstractions/Cqrs/INotification.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs @@ -1,9 +1,7 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示一个一对多发布的通知消息。 /// 通知不要求返回值,允许被零个或多个处理器消费。 /// -public interface INotification -{ -} +public interface INotification; diff --git a/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs similarity index 92% rename from GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs index 23861d1d..e3a007c8 100644 --- a/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示处理通知消息的处理器契约。 diff --git a/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs similarity index 94% rename from GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs index cd01aad3..11423c40 100644 --- a/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 定义 CQRS 请求处理前后的管道行为。 diff --git a/GFramework.Core.Abstractions/Cqrs/IRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs similarity index 71% rename from GFramework.Core.Abstractions/Cqrs/IRequest.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs index 26259fc4..dd6abb62 100644 --- a/GFramework.Core.Abstractions/Cqrs/IRequest.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs @@ -1,10 +1,8 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示一个有响应的 CQRS 请求。 /// 该接口是命令、查询以及其他请求语义的统一基接口。 /// /// 请求响应类型。 -public interface IRequest -{ -} +public interface IRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs similarity index 93% rename from GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs index 2415e282..95cdd1d1 100644 --- a/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示处理单个 CQRS 请求的处理器契约。 diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs similarity index 71% rename from GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs index 05ffa5df..5464459b 100644 --- a/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs @@ -1,10 +1,8 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示一个流式 CQRS 请求。 /// 请求处理器可以逐步产生响应序列,而不是一次性返回完整结果。 /// /// 流式响应元素类型。 -public interface IStreamRequest -{ -} +public interface IStreamRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs similarity index 94% rename from GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs index 1c6e02a7..44e7c79d 100644 --- a/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示处理流式 CQRS 请求的处理器契约。 diff --git a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs similarity index 95% rename from GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs rename to GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs index 520f9fee..8575ebd8 100644 --- a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示 CQRS 请求在管道中继续向下执行的处理委托。 diff --git a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs similarity index 87% rename from GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs index 8b791839..4fb6f735 100644 --- a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs.Notification; +namespace GFramework.Cqrs.Abstractions.Cqrs.Notification; /// /// 表示通知输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中通知类型的输入参数。 /// -public interface INotificationInput : IInput; \ No newline at end of file +public interface INotificationInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs new file mode 100644 index 00000000..edf5e1a2 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs @@ -0,0 +1,8 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; + +/// +/// 表示一个 CQRS 查询。 +/// 查询用于读取数据,不应产生副作用。 +/// +/// 查询响应类型。 +public interface IQuery : IRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs similarity index 50% rename from GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs index c505c4ff..a17e7b6a 100644 --- a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs @@ -1,6 +1,6 @@ -namespace GFramework.Core.Abstractions.Cqrs.Query; +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; /// /// 查询输入接口,定义了查询操作的输入规范 /// -public interface IQueryInput : IInput; \ No newline at end of file +public interface IQueryInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs new file mode 100644 index 00000000..150fb32c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; + +/// +/// 表示一个流式 CQRS 查询。 +/// +/// 流式响应元素类型。 +public interface IStreamQuery : IStreamRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs similarity index 88% rename from GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs index 0a0b1591..14f89b89 100644 --- a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs.Request; +namespace GFramework.Cqrs.Abstractions.Cqrs.Request; /// /// 表示请求输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中请求类型的输入参数。 /// -public interface IRequestInput : IInput; \ No newline at end of file +public interface IRequestInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Unit.cs b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs similarity index 89% rename from GFramework.Core.Abstractions/Cqrs/Unit.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Unit.cs index 7dc3da14..57d053bc 100644 --- a/GFramework.Core.Abstractions/Cqrs/Unit.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs @@ -1,4 +1,4 @@ -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示没有实际返回值的 CQRS 响应类型。 diff --git a/GFramework.Cqrs.Abstractions/Directory.Build.props b/GFramework.Cqrs.Abstractions/Directory.Build.props new file mode 100644 index 00000000..9f372d0c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Directory.Build.props @@ -0,0 +1,18 @@ + + + netstandard2.1 + true + true + preview + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj new file mode 100644 index 00000000..8e07fd2c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + GeWuYou.$(AssemblyName) + true + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute + enable + true + + + diff --git a/GFramework.Cqrs.Abstractions/GlobalUsings.cs b/GFramework.Cqrs.Abstractions/GlobalUsings.cs new file mode 100644 index 00000000..5cd04a4e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs similarity index 98% rename from GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs rename to GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs index 67e9537d..ccc6e27f 100644 --- a/GFramework.Core.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs +++ b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -13,11 +13,11 @@ using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Coroutine; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Rule; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Core.Coroutine.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Tests.Coroutine; +namespace GFramework.Cqrs.Tests.Coroutine; /// /// 的单元测试类。 diff --git a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs similarity index 99% rename from GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs rename to GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 7748227c..360fe97b 100644 --- a/GFramework.Core.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -1,12 +1,12 @@ -using System.Reflection; using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; -using GFramework.Core.Tests.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Tests.Logging; -namespace GFramework.Core.Tests.Cqrs; +namespace GFramework.Cqrs.Tests.Cqrs; /// /// 验证 CQRS 处理器自动注册在顺序与容错层面的可观察行为。 @@ -14,9 +14,6 @@ namespace GFramework.Core.Tests.Cqrs; [TestFixture] internal sealed class CqrsHandlerRegistrarTests { - private MicrosoftDiContainer? _container; - private ArchitectureContext? _context; - /// /// 初始化测试容器并重置共享状态。 /// @@ -46,6 +43,9 @@ internal sealed class CqrsHandlerRegistrarTests DeterministicNotificationHandlerState.Reset(); } + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + /// /// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。 /// diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj new file mode 100644 index 00000000..796883e4 --- /dev/null +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + $(TestTargetFrameworks) + disable + enable + false + true + + + + + + + + + + + + + + + + + diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs new file mode 100644 index 00000000..c47473b0 --- /dev/null +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +global using System; +global using System.Collections; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Linq; +global using System.Reflection; +global using System.Runtime.CompilerServices; +global using System.Threading; +global using System.Threading.Tasks; +global using GFramework.Tests.Common; +global using Microsoft.Extensions.DependencyInjection; +global using Moq; +global using NUnit.Compatibility; +global using NUnit.Framework; diff --git a/GFramework.Cqrs.Tests/Logging/TestLogger.cs b/GFramework.Cqrs.Tests/Logging/TestLogger.cs new file mode 100644 index 00000000..c0432bc7 --- /dev/null +++ b/GFramework.Cqrs.Tests/Logging/TestLogger.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace GFramework.Cqrs.Tests.Logging; + +/// +/// 供 CQRS 测试项目复用的最小日志记录器实现。 +/// +public sealed class TestLogger : AbstractLogger +{ + /// + /// 初始化测试日志记录器。 + /// + /// 日志名称。 + /// 最小日志级别。 + public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base(name, minLevel) + { + } + + /// + /// 获取当前测试期间捕获到的日志条目。 + /// + public List Logs { get; } = []; + + /// + /// 将日志写入内存,供断言使用。 + /// + /// 日志级别。 + /// 日志消息。 + /// 关联异常。 + protected override void Write(LogLevel level, string message, Exception? exception) + { + Logs.Add(new LogEntry(level, message, exception)); + } + + /// + /// 表示单条测试日志记录。 + /// + /// 日志级别。 + /// 日志消息。 + /// 关联异常。 + public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception); +} diff --git a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs similarity index 99% rename from GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index 2dc2503a..257809f0 100644 --- a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -1,11 +1,9 @@ -using System.Diagnostics; -using System.Reflection; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Mediator; /// /// Mediator高级特性专项测试 @@ -14,10 +12,6 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorAdvancedFeaturesTests { - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - [SetUp] public void SetUp() { @@ -46,6 +40,10 @@ public class MediatorAdvancedFeaturesTests _container = null; } + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + [Test] public async Task Request_With_Validation_Behavior_Should_Validate_Input() diff --git a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs similarity index 99% rename from GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index e176cce5..e403735e 100644 --- a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,15 +1,13 @@ -using System.Diagnostics; -using System.Reflection; using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Architectures; using GFramework.Core.Command; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; -namespace GFramework.Core.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Mediator; /// /// Mediator与架构上下文集成测试 @@ -18,11 +16,6 @@ namespace GFramework.Core.Tests.Mediator; [TestFixture] public class MediatorArchitectureIntegrationTests { - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - [SetUp] public void SetUp() { @@ -56,6 +49,11 @@ public class MediatorArchitectureIntegrationTests _commandBus = null; } + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + [Test] public async Task Handler_Can_Access_Architecture_Context() { diff --git a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs similarity index 99% rename from GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index 27dfed5c..b0b510d6 100644 --- a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -1,8 +1,4 @@ -using System.Diagnostics; -using System.Reflection; -using System.Runtime.CompilerServices; using GFramework.Core.Abstractions.Architectures; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Events; using GFramework.Core.Architectures; using GFramework.Core.Command; @@ -11,23 +7,14 @@ using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs; using ICommand = GFramework.Core.Abstractions.Command.ICommand; -using Unit = GFramework.Core.Abstractions.Cqrs.Unit; -namespace GFramework.Core.Tests.Mediator; +namespace GFramework.Cqrs.Tests.Mediator; [TestFixture] public class MediatorComprehensiveTests { - private AsyncQueryExecutor? _asyncQueryBus; - private CommandExecutor? _commandBus; - private MicrosoftDiContainer? _container; - - private ArchitectureContext? _context; - private DefaultEnvironment? _environment; - private EventBus? _eventBus; - private QueryExecutor? _queryBus; - /// /// 测试初始化方法,在每个测试方法执行前运行。 /// 负责初始化日志工厂、依赖注入容器、自有 CQRS 处理器以及各种总线服务。 @@ -82,6 +69,15 @@ public class MediatorComprehensiveTests _environment = null; } + private AsyncQueryExecutor? _asyncQueryBus; + private CommandExecutor? _commandBus; + private MicrosoftDiContainer? _container; + + private ArchitectureContext? _context; + private DefaultEnvironment? _environment; + private EventBus? _eventBus; + private QueryExecutor? _queryBus; + /// /// 测试SendRequestAsync方法在请求有效时返回结果 /// diff --git a/GFramework.Cqrs/GFramework.Cqrs.csproj b/GFramework.Cqrs/GFramework.Cqrs.csproj new file mode 100644 index 00000000..9f002283 --- /dev/null +++ b/GFramework.Cqrs/GFramework.Cqrs.csproj @@ -0,0 +1,16 @@ + + + + GeWuYou.$(AssemblyName) + net8.0;net9.0;net10.0 + disable + enable + true + true + + + + + + + diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs index 1c2699ae..71d523eb 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记类型允许为带映射特性的导出集合生成批量注册代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs index 3b73af7b..4e56cd9b 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记场景根节点类型,Source Generator 会生成场景行为样板代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs index 56957fb8..0356c43c 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记 UI 页面类型,Source Generator 会生成页面行为样板代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs index dd809fc4..c4fe14a2 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 声明导出集合应当转发到哪个注册器成员及其方法。 diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index b50b0d73..2c6dc972 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -11,10 +11,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -88,10 +88,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -137,10 +137,10 @@ public class AutoSceneGeneratorTests const string source = """ #nullable enable using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -225,10 +225,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -279,10 +279,10 @@ public class AutoSceneGeneratorTests const string source = """ using System; using GFramework.Game.Abstractions.Scene; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs index 8e7d1115..2b8aca67 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs @@ -11,10 +11,10 @@ public class AutoUiPageGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute @@ -100,10 +100,10 @@ public class AutoUiPageGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute @@ -183,10 +183,10 @@ public class AutoUiPageGeneratorTests const string source = """ #nullable enable using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 3a67d3c5..befdcf59 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -13,9 +13,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -86,9 +86,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -141,9 +141,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -207,9 +207,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -284,9 +284,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -344,9 +344,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -414,9 +414,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -482,9 +482,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -549,9 +549,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -604,9 +604,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -659,9 +659,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -715,9 +715,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs index f2920be4..ee389f0e 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs @@ -18,7 +18,8 @@ namespace GFramework.Godot.SourceGenerators.Behavior; public sealed class AutoSceneGenerator : IIncrementalGenerator { private const string AutoSceneAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoSceneAttribute"; + private static readonly string[] GeneratedMemberNames = [ "SceneKeyStr", diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs index 1e0fe7f6..ca193956 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs @@ -12,7 +12,7 @@ namespace GFramework.Godot.SourceGenerators.Behavior; public sealed class AutoUiPageGenerator : IIncrementalGenerator { private const string AutoUiPageAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoUiPageAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoUiPageAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index 12589767..93219dcb 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -18,10 +18,10 @@ namespace GFramework.Godot.SourceGenerators.Registration; public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator { private const string AutoRegisterExportedCollectionsAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoRegisterExportedCollectionsAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoRegisterExportedCollectionsAttribute"; private const string RegisterExportedCollectionAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.RegisterExportedCollectionAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.RegisterExportedCollectionAttribute"; private const string GeneratedMethodName = "__RegisterExportedCollections_Generated"; diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index 98d57936..6805d2e7 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -1,10 +1,10 @@ -using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Rule; +using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Extensions; -using GFramework.Core.Cqrs.Extensions; +using GFramework.Core.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Godot.Coroutine; diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs new file mode 100644 index 00000000..57d4effd --- /dev/null +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Architectures; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Tests.Common; + +/// +/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 +/// +/// +/// 该测试基础设施位于独立模块中,避免多个测试项目复制同一份反射绑定与默认 runtime 接线逻辑。 +/// 测试应通过该入口驱动注册流程,而不是各自维护一份实现细节副本。 +/// +public static class CqrsTestRuntime +{ + private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)!; + + private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType + .GetMethod( + "RegisterHandlers", + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static, + binder: null, + [ + typeof(IIocContainer), + typeof(IEnumerable), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + + private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsDispatcher", + throwOnError: true)!; + + private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher constructor."); + + private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", + throwOnError: true)!; + + private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = + DefaultCqrsHandlerRegistrarType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar constructor."); + + /// + /// 为裸测试容器补齐默认 CQRS runtime seam。 + /// + /// 目标测试容器。 + /// + /// 反射调用底层 CQRS runtime 或注册器构造函数失败时抛出。 + /// + /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, + /// 而无需完整启动服务模块管理器。 + /// 该方法按服务类型执行幂等注册,只会补齐当前容器中尚未接线的 CQRS 基础设施。 + /// + public static void RegisterInfrastructure(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + if (container.Get() is null) + { + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger(CqrsDispatcherType.Name); + var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + container.Register(runtime); + } + + if (container.Get() is null) + { + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(DefaultCqrsHandlerRegistrarType.Name); + var registrar = + (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + container.Register(registrar); + } + } + + /// + /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 + /// + /// 承载处理器映射的测试容器。 + /// 要扫描的程序集集合。 + /// + /// 。 + /// + /// 反射调用底层 CQRS 处理器注册入口失败时抛出。 + /// + /// 该入口会自动调用 ,因此测试通常无需预先手动接线 CQRS 基础设施。 + /// 程序集去重与空元素过滤由生产注册入口统一处理,避免测试辅助层复制相同筛选逻辑。 + /// + public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(assemblies); + + RegisterInfrastructure(container); + + var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + RegisterHandlersMethod.Invoke( + null, + [container, assemblies, logger]); + } +} diff --git a/GFramework.Tests.Common/GFramework.Tests.Common.csproj b/GFramework.Tests.Common/GFramework.Tests.Common.csproj new file mode 100644 index 00000000..51f8fba9 --- /dev/null +++ b/GFramework.Tests.Common/GFramework.Tests.Common.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + disable + enable + true + false + + + + + + + + + diff --git a/GFramework.csproj b/GFramework.csproj index 76f9a088..b9c6c7a4 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -63,6 +63,10 @@ + + + + @@ -104,6 +108,10 @@ + + + + @@ -131,6 +139,10 @@ + + + + diff --git a/GFramework.sln b/GFramework.sln index 6d4eb45f..bc64ce21 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -38,6 +38,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGene EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstractions", "GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj", "{69C06523-98AA-49DE-95D4-4BF203716DD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramework.Cqrs\GFramework.Cqrs.csproj", "{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Tests", "GFramework.Cqrs.Tests\GFramework.Cqrs.Tests.csproj", "{29037A55-9A89-425C-AB33-D44872B2E601}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Tests.Common", "GFramework.Tests.Common\GFramework.Tests.Common.csproj", "{1100EE3E-A12D-4DE5-ABA8-591D3126570B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -276,6 +284,54 @@ Global {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x64.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x86.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|Any CPU.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x64.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x64.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x86.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md b/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md index 2d73f531..8d1ca366 100644 --- a/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md +++ b/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md @@ -13,12 +13,13 @@ `AutoRegisterExportedCollections` 会把这类样板收敛成声明式配置。 它特别适合 `GameEntryPoint`、资源根节点、配置引导节点这类“导出即注册”的场景。 +相关特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp using System.Collections.Generic; -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; public interface IKeyValue diff --git a/docs/zh-CN/source-generators/auto-scene-generator.md b/docs/zh-CN/source-generators/auto-scene-generator.md index 1a2ff881..92927daa 100644 --- a/docs/zh-CN/source-generators/auto-scene-generator.md +++ b/docs/zh-CN/source-generators/auto-scene-generator.md @@ -12,11 +12,12 @@ - `GetScene()` 包装方法 `AutoScene` 会在编译期生成这些固定样板。 +该特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; diff --git a/docs/zh-CN/source-generators/auto-ui-page-generator.md b/docs/zh-CN/source-generators/auto-ui-page-generator.md index b455eab8..1c1ecd48 100644 --- a/docs/zh-CN/source-generators/auto-ui-page-generator.md +++ b/docs/zh-CN/source-generators/auto-ui-page-generator.md @@ -13,11 +13,12 @@ - `GetPage()` 工厂包装 `AutoUiPage` 会把这部分样板迁移到编译期生成。 +该特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -123,7 +124,7 @@ partial class MainMenu ## 组合示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using GFramework.SourceGenerators.Abstractions.Rule; using Godot; diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md index 8fd91e66..ea62aad0 100644 --- a/docs/zh-CN/source-generators/index.md +++ b/docs/zh-CN/source-generators/index.md @@ -610,7 +610,7 @@ AutoUiPage 生成器为 Godot 页面节点自动生成 `UiKeyStr`、缓存的 `I ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -635,7 +635,7 @@ AutoScene 生成器为场景根节点自动生成 `SceneKeyStr`、缓存的 `ISc ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -660,7 +660,7 @@ AutoRegisterExportedCollections 生成器为 Godot 导出集合自动生成批 ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; [AutoRegisterExportedCollections]