Merge pull request #224 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-5

Refactor/cqrs architecture decoupling todo 5
This commit is contained in:
gewuyou 2026-04-15 19:18:13 +08:00 committed by GitHub
commit f7b4ae9995
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1159 additions and 422 deletions

View File

@ -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 \

View File

@ -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
/// <remarks>
/// 这是迁移后的推荐命令入口。无返回值命令应实现 <c>IRequest&lt;Unit&gt;</c>,并优先通过 <see cref="SendAsync{TCommand}(TCommand,CancellationToken)" /> 调用。
/// </remarks>
TResponse SendCommand<TResponse>(Cqrs.Command.ICommand<TResponse> command);
TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command);
/// <summary>
@ -147,7 +147,8 @@ public interface IArchitectureContext
/// <param name="command">要发送的 CQRS 命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>包含命令执行结果的值任务。</returns>
ValueTask<TResponse> SendCommandAsync<TResponse>(Cqrs.Command.ICommand<TResponse> command,
ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default);
@ -176,7 +177,7 @@ public interface IArchitectureContext
/// <remarks>
/// 这是迁移后的推荐查询入口。新查询应优先实现 <c>GFramework.Core.Abstractions.Cqrs.Query.IQuery&lt;TResponse&gt;</c>。
/// </remarks>
TResponse SendQuery<TResponse>(Cqrs.Query.IQuery<TResponse> query);
TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query);
/// <summary>
/// 异步发送一个旧版查询请求。
@ -193,7 +194,7 @@ public interface IArchitectureContext
/// <param name="query">要发送的 CQRS 查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>包含查询结果的值任务。</returns>
ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Query.IQuery<TResponse> query,
ValueTask<TResponse> SendQueryAsync<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default);
/// <summary>

View File

@ -1,25 +0,0 @@
namespace GFramework.Core.Abstractions.Cqrs.Command;
/// <summary>
/// 表示一个 CQRS 命令。
/// 命令通常用于修改系统状态。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
public interface ICommand<out TResponse> : IRequest<TResponse>
{
}
/// <summary>
/// 表示一个无显式返回值的 CQRS 命令。
/// </summary>
public interface ICommand : ICommand<Unit>
{
}
/// <summary>
/// 表示一个流式 CQRS 命令。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
public interface IStreamCommand<out TResponse> : IStreamRequest<TResponse>
{
}

View File

@ -0,0 +1,52 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Abstractions.Cqrs;
/// <summary>
/// 定义架构上下文使用的 CQRS runtime seam。
/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦,
/// 使 <see cref="IArchitectureContext" /> 不再直接依赖某个固定的 runtime 类型。
/// </summary>
public interface ICqrsRuntime
{
/// <summary>
/// 发送请求并返回响应。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。</param>
/// <param name="request">要分发的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
ValueTask<TResponse> SendAsync<TResponse>(
IArchitectureContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default);
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示通知分发完成的值任务。</returns>
ValueTask PublishAsync<TNotification>(
IArchitectureContext context,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification;
/// <summary>
/// 创建流式请求的异步响应序列。
/// </summary>
/// <typeparam name="TResponse">流元素类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>按需生成的异步响应序列。</returns>
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IArchitectureContext context,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default);
}

View File

@ -1,18 +0,0 @@
namespace GFramework.Core.Abstractions.Cqrs.Query;
/// <summary>
/// 表示一个 CQRS 查询。
/// 查询用于读取数据,不应产生副作用。
/// </summary>
/// <typeparam name="TResponse">查询响应类型。</typeparam>
public interface IQuery<out TResponse> : IRequest<TResponse>
{
}
/// <summary>
/// 表示一个流式 CQRS 查询。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
public interface IStreamQuery<out TResponse> : IStreamRequest<TResponse>
{
}

View File

@ -17,6 +17,9 @@
<ItemGroup>
<Using Include="GFramework.Core.Abstractions"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Meziantou.Analyzer" Version="3.0.46">
<PrivateAssets>all</PrivateAssets>

View File

@ -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;
/// <summary>
/// 初始化日志工厂和共享测试状态。
/// </summary>
@ -39,6 +38,8 @@ public sealed class ArchitectureAdditionalCqrsHandlersTests
"LoggerFactoryResolver.Provider should be captured during setup.");
}
private ILoggerFactoryProvider? _previousLoggerFactoryProvider;
/// <summary>
/// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。
/// </summary>
@ -197,4 +198,4 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl
});
return handler.Object;
}
}
}

View File

@ -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;
/// <summary>
/// 测试构造函数在所有参数都有效时不应抛出异常
@ -298,6 +302,76 @@ public class ArchitectureContextTests
Assert.That(environment, Is.Not.Null);
Assert.That(environment, Is.InstanceOf<IEnvironment>());
}
/// <summary>
/// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。
/// </summary>
[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<ICqrsRuntime>(MockBehavior.Strict);
var container = new Mock<IIocContainer>(MockBehavior.Strict);
runtime.Setup(mockRuntime => mockRuntime.SendAsync(
It.IsAny<IArchitectureContext>(),
It.IsAny<IRequest<int>>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<int>(42));
container.Setup(mockContainer => mockContainer.Get<ICqrsRuntime>())
.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<ICqrsRuntime>(), Times.Once);
runtime.Verify(
mockRuntime => mockRuntime.SendAsync(
It.IsAny<IArchitectureContext>(),
It.IsAny<IRequest<int>>(),
It.IsAny<CancellationToken>()),
Times.Exactly(requests.Length));
}
private sealed class TestCqrsRequest : IRequest<int>
{
}
}
#region Test Classes
@ -442,4 +516,4 @@ public class TestEventV2
public int Data { get; init; }
}
#endregion
#endregion

View File

@ -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
/// <summary>
/// 用于验证管道行为注册是否生效的测试请求。
/// </summary>
public sealed class ModuleBehaviorRequest : GfCqrs.IRequest<string>
public sealed class ModuleBehaviorRequest : IRequest<string>
{
}
/// <summary>
/// 处理测试请求的处理器。
/// </summary>
public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler<ModuleBehaviorRequest, string>
public sealed class ModuleBehaviorRequestHandler : IRequestHandler<ModuleBehaviorRequest, string>
{
/// <summary>
/// 返回固定结果,便于聚焦验证管道行为是否执行。
@ -177,8 +177,8 @@ public sealed class ModuleBehaviorRequestHandler : GfCqrs.IRequestHandler<Module
/// </summary>
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
public sealed class TrackingPipelineBehavior<TRequest, TResponse> : GfCqrs.IPipelineBehavior<TRequest, TResponse>
where TRequest : GfCqrs.IRequest<TResponse>
public sealed class TrackingPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <summary>
/// 获取当前测试进程中该请求类型对应的行为触发次数。
@ -193,7 +193,7 @@ public sealed class TrackingPipelineBehavior<TRequest, TResponse> : GfCqrs.IPipe
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理器的响应结果。</returns>
public async ValueTask<TResponse> Handle(
TRequest message, GfCqrs.MessageHandlerDelegate<TRequest, TResponse> next,
TRequest message, MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken)
{
InvocationCount++;

View File

@ -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<TResponse> SendCommandAsync<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command,
/// <summary>
/// 测试桩:异步发送 CQRS 命令并返回响应。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendCommand<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command)
/// <summary>
/// 测试桩:同步发送 CQRS 命令并返回响应。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendQueryAsync<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query,
/// <summary>
/// 测试桩:异步发送 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public TResponse SendQuery<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query)
/// <summary>
/// 测试桩:同步发送 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
}

View File

@ -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
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command,
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
@ -441,7 +442,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendCommand<TResponse>(Abstractions.Cqrs.Command.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
}
@ -454,7 +455,8 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query,
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
@ -467,7 +469,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendQuery<TResponse>(Abstractions.Cqrs.Query.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
}

View File

@ -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<Tes
Executed = true;
return Task.FromResult(input.Value * 3);
}
}
}

View File

@ -1,5 +1,5 @@
using GFramework.Core.Abstractions.Cqrs.Command;
using GFramework.Core.Command;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Tests.Command;
@ -261,4 +261,4 @@ public sealed class TestAsyncCommandWithResult : AbstractAsyncCommand<TestComman
Executed = true;
return Task.FromResult(input.Value * 2);
}
}
}

View File

@ -0,0 +1,38 @@
// 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.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Cqrs;
/// <summary>
/// 为容器层测试提供可扫描的最小通知夹具。
/// </summary>
internal sealed record DeterministicOrderNotification : INotification;
/// <summary>
/// 供容器注册测试验证程序集扫描结果的通知处理器。
/// </summary>
internal sealed class DeterministicOrderNotificationHandler : INotificationHandler<DeterministicOrderNotification>
{
/// <summary>
/// 无副作用地消费通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}

View File

@ -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.");
}
/// <summary>

View File

@ -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;
/// <summary>
/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。
/// </summary>
/// <remarks>
/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法,
/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。
/// </remarks>
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<Assembly>),
typeof(ILogger)
],
modifiers: null)
?? throw new InvalidOperationException(
"Failed to locate CqrsHandlerRegistrar.RegisterHandlers.");
/// <summary>
/// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。
/// </summary>
/// <param name="container">承载处理器映射的测试容器。</param>
/// <param name="assemblies">要扫描的程序集集合。</param>
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]);
}
}

View File

@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="Scriban" Version="7.1.0" />
<ProjectReference Include="..\GFramework.Tests.Common\GFramework.Tests.Common.csproj"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>

View File

@ -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;

View File

@ -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));
}
/// <summary>
/// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。
/// </summary>
[Test]
public void RegisterHandlers_Should_Not_Duplicate_Cqrs_Infrastructure_When_It_Is_Already_Registered()
{
Assert.That(_container.GetAll<ICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
CqrsTestRuntime.RegisterHandlers(_container);
Assert.That(_container.GetAll<ICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
}
/// <summary>
/// 测试当没有实例时获取应返回 null 的功能
/// </summary>
@ -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<DeterministicOrderNotification>)),
Is.False);
// Clear 会移除测试手工补齐的 CQRS seam需要先恢复基础设施再验证程序集去重状态是否已重置。
CqrsTestRuntime.RegisterInfrastructure(_container);
_container.RegisterCqrsHandlersFromAssembly(assembly);
Assert.That(

View File

@ -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
/// 获取或设置双倍值
/// </summary>
public int DoubleValue { get; init; }
}
}

View File

@ -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
/// 获取或设置双倍值
/// </summary>
public int DoubleValue { get; init; }
}
}

View File

@ -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<TestQueryInput, string>
{
return $"Result: {input.Value * 2}";
}
}
}

View File

@ -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;
/// <summary>
/// 架构上下文类,提供对系统、模型、工具等组件的访问以及命令、查询、事件的执行管理
/// </summary>
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<ICqrsRuntime> _cqrsRuntime;
private readonly ConcurrentDictionary<Type, object> _serviceCache = new();
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContext));
private CqrsDispatcher? _cqrsDispatcher;
/// <summary>
/// 初始化新的架构上下文,并绑定其依赖容器。
/// </summary>
/// <param name="container">
/// 当前架构使用的 IOC 容器。
/// CQRS runtime 与其他框架服务会通过该容器延迟解析,以避免在上下文构造阶段强制拉起整条运行时链路。
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="container" /> 为 <see langword="null" />。</exception>
public ArchitectureContext(IIocContainer container)
{
_container = container ?? throw new ArgumentNullException(nameof(container));
_cqrsRuntime = new Lazy<ICqrsRuntime>(
ResolveCqrsRuntime,
LazyThreadSafetyMode.ExecutionAndPublication);
}
#region CQRS Integration
/// <summary>
/// 获取 CQRS 运行时分发器(延迟初始化)。
/// 获取 CQRS runtime seam
/// </summary>
private CqrsDispatcher CqrsDispatcher => _cqrsDispatcher ??= new CqrsDispatcher(_container, this, _logger);
/// <remarks>
/// 该实例会在首次访问时从容器解析,并通过 <see cref="Lazy{T}" /> 保证并发场景下只执行一次初始化,
/// 避免多个请求线程重复触发同一个 runtime 的容器解析。
/// </remarks>
private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value;
/// <summary>
/// 从容器解析当前架构上下文依赖的 CQRS runtime。
/// </summary>
/// <returns>已注册的 CQRS runtime 实例。</returns>
/// <exception cref="InvalidOperationException">容器中未注册 <see cref="ICqrsRuntime" />。</exception>
private ICqrsRuntime ResolveCqrsRuntime()
{
return _container.Get<ICqrsRuntime>() ?? throw new InvalidOperationException("ICqrsRuntime not registered");
}
/// <summary>
/// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存
@ -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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -151,7 +176,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="query">要发送的查询</param>
/// <returns>查询结果</returns>
public TResult SendQuery<TResult>(Abstractions.Query.IQuery<TResult> query)
public TResult SendQuery<TResult>(IQuery<TResult> query)
{
if (query == null) throw new ArgumentNullException(nameof(query));
var queryBus = GetOrCache<IQueryExecutor>();
@ -165,7 +190,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <returns>查询结果</returns>
public TResponse SendQuery<TResponse>(GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
}
@ -191,7 +216,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
/// <param name="query">要发送的查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含查询结果的ValueTask</returns>
public async ValueTask<TResponse> SendQueryAsync<TResponse>(GFramework.Core.Abstractions.Cqrs.Query.IQuery<TResponse> query,
public async ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
@ -327,7 +353,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
/// <param name="command">要发送的命令对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含命令执行结果的ValueTask</returns>
public async ValueTask<TResponse> SendCommandAsync<TResponse>(GFramework.Core.Abstractions.Cqrs.Command.ICommand<TResponse> command,
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(command);
@ -366,7 +393,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <returns>命令执行结果</returns>
public TResponse SendCommand<TResponse>(GFramework.Core.Abstractions.Cqrs.Command.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
}
@ -388,7 +415,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
/// <typeparam name="TResult">命令执行结果类型</typeparam>
/// <param name="command">要发送的命令</param>
/// <returns>命令执行结果</returns>
public TResult SendCommand<TResult>(Abstractions.Command.ICommand<TResult> command)
public TResult SendCommand<TResult>(ICommand<TResult> command)
{
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();

View File

@ -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>(TInput input) : ContextAwareB
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync(TInput input);
}
}

View File

@ -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, TResult>(TInput input) : Cont
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作且包含结果的任务</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}
}

View File

@ -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;
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法
/// </summary>
void GFramework.Core.Abstractions.Command.ICommand.Execute()
void ICommand.Execute()
{
OnExecute(input);
}

View File

@ -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;
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand<TResult>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法
/// </summary>
/// <returns>命令执行后的结果</returns>
TResult GFramework.Core.Abstractions.Command.ICommand<TResult>.Execute()
TResult Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}

View File

@ -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接口作为命令模式中的输入参数载体
/// 通常用于不需要额外输入参数的简单命令操作
/// </remarks>
public sealed class EmptyCommandInput : ICommandInput;
public sealed class EmptyCommandInput : ICommandInput;

View File

@ -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;
/// <summary>
/// 提供 CQRS 命令与协程集成的扩展方法。

View File

@ -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;
/// <summary>
/// 提供 CQRS 命令与协程集成的扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions" />。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="CqrsCoroutineExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[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
{
/// <summary>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
/// </summary>
internal sealed class CqrsDispatcher(
IIocContainer container,
IArchitectureContext context,
ILogger logger)
ILogger logger) : ICqrsRuntime
{
private delegate ValueTask<object?> RequestInvoker(object handler, object request, CancellationToken cancellationToken);
private delegate ValueTask<object?> RequestPipelineInvoker(
object handler,
IReadOnlyList<object> 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<Type, NotificationInvoker> NotificationInvokers = new();
private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new();
// 进程级缓存:缓存流式请求调用委托,避免每次创建流时重复解析反射签名。
private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers =
new();
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="notification">通知对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
public async ValueTask PublishAsync<TNotification>(
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);
}
}
/// <summary>
/// 发送请求并返回结果。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="request">请求对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
public async ValueTask<TResponse> SendAsync<TResponse>(
IArchitectureContext context,
IRequest<TResponse> 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;
}
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="notification">通知对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
public async ValueTask PublishAsync<TNotification>(
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);
}
}
/// <summary>
/// 创建流式请求并返回异步响应序列。
/// </summary>
/// <typeparam name="TResponse">响应元素类型。</typeparam>
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
/// <param name="request">流式请求对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应序列。</returns>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IArchitectureContext context,
IStreamRequest<TResponse> 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(
/// 为上下文感知处理器注入当前架构上下文。
/// </summary>
/// <param name="handler">处理器实例。</param>
private void PrepareHandler(object handler)
/// <param name="context">当前架构上下文。</param>
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<object?> RequestInvoker(object handler, object request,
CancellationToken cancellationToken);
private delegate ValueTask<object?> RequestPipelineInvoker(
object handler,
IReadOnlyList<object> 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);
}

View File

@ -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;

View File

@ -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;
/// <summary>
/// 默认的 CQRS 处理器注册器实现。
/// 该适配器把容器公开的 handler 接入入口转发到现有的注册流水线,
/// 使容器主路径只依赖 <see cref="ICqrsHandlerRegistrar" /> 抽象。
/// </summary>
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));
/// <summary>
/// 按当前 runtime 约定扫描并注册处理器程序集。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
public void RegisterHandlers(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
CqrsHandlerRegistrar.RegisterHandlers(_container, assemblies, _logger);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。

View File

@ -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;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一扩展方法。

View File

@ -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;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。

View File

@ -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;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions" />。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="ContextAwareCqrsCommandExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[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
{
/// <summary>

View File

@ -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;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一接口扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions" />。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="ContextAwareCqrsExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[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
{
/// <summary>

View File

@ -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;
/// <summary>
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions" />。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="ContextAwareCqrsQueryExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[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
{
/// <summary>

View File

@ -9,6 +9,7 @@
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
</ItemGroup>
<ItemGroup>

View File

@ -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
/// <summary>
/// 获取当前容器中已注册的 CQRS 处理器注册器。
/// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定,
/// 避免在容器未冻结前依赖完整的服务提供者构建流程。
/// </summary>
/// <returns>已注册的 CQRS 处理器注册器实例。</returns>
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 处理器注册器实例时抛出。</exception>
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);
}
/// <summary>
/// 获取指定泛型类型的服务实例
/// 返回第一个匹配的注册实例如果不存在则返回null

View File

@ -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<TInput, TResult>(
/// <param name="input">查询输入参数</param>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}
}

View File

@ -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;
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TInput, TResult>(TInput input) : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>

View File

@ -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;
/// <remarks>
/// 该类实现了IQueryInput接口作为占位符使用适用于那些不需要额外输入参数的查询场景
/// </remarks>
public sealed class EmptyQueryInput : IQueryInput;
public sealed class EmptyQueryInput : IQueryInput;

View File

@ -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;
/// <summary>
/// CQRS runtime 模块,用于把默认请求分发器与处理器注册器接入架构容器。
/// 该模块在架构初始化早期完成注册,保证用户初始化阶段即可使用 CQRS 入口与 handler 自动接入能力。
/// </summary>
public sealed class CqrsRuntimeModule : IServiceModule
{
/// <summary>
/// 获取模块名称。
/// </summary>
public string ModuleName => nameof(CqrsRuntimeModule);
/// <summary>
/// 获取模块优先级。
/// CQRS runtime 需要先于架构默认 handler 扫描路径可用,因此放在基础总线模块之后、用户初始化之前注册。
/// </summary>
public int Priority => 15;
/// <summary>
/// 获取模块启用状态,默认启用。
/// </summary>
public bool IsEnabled => true;
/// <summary>
/// 注册默认 CQRS runtime seam 实现。
/// </summary>
/// <param name="container">目标依赖注入容器。</param>
public void Register(IIocContainer container)
{
ArgumentNullException.ThrowIfNull(container);
var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsDispatcher));
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(DefaultCqrsHandlerRegistrar));
container.Register<ICqrsRuntime>(new CqrsDispatcher(container, dispatcherLogger));
container.Register<ICqrsHandlerRegistrar>(new DefaultCqrsHandlerRegistrar(container, registrarLogger));
}
/// <summary>
/// 初始化模块。
/// </summary>
public void Initialize()
{
}
/// <summary>
/// 异步销毁模块。
/// </summary>
/// <returns>已完成的值任务。</returns>
public ValueTask DestroyAsync()
{
return ValueTask.CompletedTask;
}
}

View File

@ -42,7 +42,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager
/// <summary>
/// 注册内置服务模块,并根据优先级排序后完成服务注册。
/// 内置模块包括事件总线、命令执行器、查询执行器等核心模块。
/// 内置模块包括事件总线、命令执行器、CQRS runtime、查询执行器等核心模块。
/// 同时注册通过 ArchitectureModuleRegistry 自动注册的外部模块。
/// </summary>
/// <param name="container">IoC容器实例用于模块服务注册。</param>
@ -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");
}
}
}

View File

@ -0,0 +1,13 @@
namespace GFramework.Cqrs.Abstractions.Cqrs.Command;
/// <summary>
/// 表示一个 CQRS 命令。
/// 命令通常用于修改系统状态。
/// </summary>
/// <typeparam name="TResponse">命令响应类型。</typeparam>
public interface ICommand<out TResponse> : IRequest<TResponse>;
/// <summary>
/// 表示一个无显式返回值的 CQRS 命令。
/// </summary>
public interface ICommand : ICommand<Unit>;

View File

@ -1,7 +1,7 @@
namespace GFramework.Core.Abstractions.Cqrs.Command;
namespace GFramework.Cqrs.Abstractions.Cqrs.Command;
/// <summary>
/// 命令输入接口,定义命令模式中输入数据的契约
/// 该接口作为标记接口使用,不包含任何成员定义
/// </summary>
public interface ICommandInput : IInput;
public interface ICommandInput : IInput;

View File

@ -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;
/// <summary>
/// 表示一个流式 CQRS 命令。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
public interface IStreamCommand<out TResponse> : IStreamRequest<TResponse>;

View File

@ -0,0 +1,17 @@
using System.Reflection;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义 CQRS 处理器程序集接入的 runtime seam。
/// 该抽象负责承接“生成注册器优先、反射扫描回退”的处理器注册流程,
/// 让容器与架构启动链不再直接依赖固定的注册实现类型。
/// </summary>
public interface ICqrsHandlerRegistrar
{
/// <summary>
/// 扫描并注册指定程序集集合中的 CQRS 处理器。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
void RegisterHandlers(IEnumerable<Assembly> assemblies);
}

View File

@ -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;
/// <summary>
/// 表示输入数据的标记接口。
/// 该接口用于标识各类CQRS模式中的输入参数类型。
/// </summary>
public interface IInput;
public interface IInput;

View File

@ -1,9 +1,7 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示一个一对多发布的通知消息。
/// 通知不要求返回值,允许被零个或多个处理器消费。
/// </summary>
public interface INotification
{
}
public interface INotification;

View File

@ -1,4 +1,4 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示处理通知消息的处理器契约。

View File

@ -1,4 +1,4 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义 CQRS 请求处理前后的管道行为。

View File

@ -1,10 +1,8 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示一个有响应的 CQRS 请求。
/// 该接口是命令、查询以及其他请求语义的统一基接口。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
public interface IRequest<out TResponse>
{
}
public interface IRequest<out TResponse>;

View File

@ -1,4 +1,4 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示处理单个 CQRS 请求的处理器契约。

View File

@ -1,10 +1,8 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示一个流式 CQRS 请求。
/// 请求处理器可以逐步产生响应序列,而不是一次性返回完整结果。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
public interface IStreamRequest<out TResponse>
{
}
public interface IStreamRequest<out TResponse>;

View File

@ -1,4 +1,4 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示处理流式 CQRS 请求的处理器契约。

View File

@ -1,4 +1,4 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示 CQRS 请求在管道中继续向下执行的处理委托。

View File

@ -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;
/// <summary>
/// 表示通知输入数据的标记接口。
/// 该接口继承自 IInput用于标识CQRS模式中通知类型的输入参数。
/// </summary>
public interface INotificationInput : IInput;
public interface INotificationInput : IInput;

View File

@ -0,0 +1,8 @@
namespace GFramework.Cqrs.Abstractions.Cqrs.Query;
/// <summary>
/// 表示一个 CQRS 查询。
/// 查询用于读取数据,不应产生副作用。
/// </summary>
/// <typeparam name="TResponse">查询响应类型。</typeparam>
public interface IQuery<out TResponse> : IRequest<TResponse>;

View File

@ -1,6 +1,6 @@
namespace GFramework.Core.Abstractions.Cqrs.Query;
namespace GFramework.Cqrs.Abstractions.Cqrs.Query;
/// <summary>
/// 查询输入接口,定义了查询操作的输入规范
/// </summary>
public interface IQueryInput : IInput;
public interface IQueryInput : IInput;

View File

@ -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;
/// <summary>
/// 表示一个流式 CQRS 查询。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
public interface IStreamQuery<out TResponse> : IStreamRequest<TResponse>;

View File

@ -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;
/// <summary>
/// 表示请求输入数据的标记接口。
/// 该接口继承自 IInput用于标识CQRS模式中请求类型的输入参数。
/// </summary>
public interface IRequestInput : IInput;
public interface IRequestInput : IInput;

View File

@ -1,4 +1,4 @@
namespace GFramework.Core.Abstractions.Cqrs;
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示没有实际返回值的 CQRS 响应类型。

View File

@ -0,0 +1,18 @@
<Project>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="3.0.46">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Polyfill" Version="1.0.109">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>GeWuYou.$(AssemblyName)</PackageId>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills>
<Nullable>enable</Nullable>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,3 @@
global using System.Collections.Generic;
global using System.Threading;
global using System.Threading.Tasks;

View File

@ -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;
/// <summary>
/// <see cref="CqrsCoroutineExtensions" /> 的单元测试类。

View File

@ -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;
/// <summary>
/// 验证 CQRS 处理器自动注册在顺序与容错层面的可观察行为。
@ -14,9 +14,6 @@ namespace GFramework.Core.Tests.Cqrs;
[TestFixture]
internal sealed class CqrsHandlerRegistrarTests
{
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 初始化测试容器并重置共享状态。
/// </summary>
@ -46,6 +43,9 @@ internal sealed class CqrsHandlerRegistrarTests
DeterministicNotificationHandlerState.Reset();
}
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
/// </summary>

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>
<PackageReference Include="Moq" Version="4.20.72"/>
<PackageReference Include="NUnit" Version="4.5.1"/>
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Tests.Common\GFramework.Tests.Common.csproj"/>
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
</ItemGroup>
</Project>

View File

@ -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;

View File

@ -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;
/// <summary>
/// 供 CQRS 测试项目复用的最小日志记录器实现。
/// </summary>
public sealed class TestLogger : AbstractLogger
{
/// <summary>
/// 初始化测试日志记录器。
/// </summary>
/// <param name="name">日志名称。</param>
/// <param name="minLevel">最小日志级别。</param>
public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base(name, minLevel)
{
}
/// <summary>
/// 获取当前测试期间捕获到的日志条目。
/// </summary>
public List<LogEntry> Logs { get; } = [];
/// <summary>
/// 将日志写入内存,供断言使用。
/// </summary>
/// <param name="level">日志级别。</param>
/// <param name="message">日志消息。</param>
/// <param name="exception">关联异常。</param>
protected override void Write(LogLevel level, string message, Exception? exception)
{
Logs.Add(new LogEntry(level, message, exception));
}
/// <summary>
/// 表示单条测试日志记录。
/// </summary>
/// <param name="Level">日志级别。</param>
/// <param name="Message">日志消息。</param>
/// <param name="Exception">关联异常。</param>
public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception);
}

View File

@ -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;
/// <summary>
/// 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()

View File

@ -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;
/// <summary>
/// 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()
{

View File

@ -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;
/// <summary>
/// 测试初始化方法,在每个测试方法执行前运行。
/// 负责初始化日志工厂、依赖注入容器、自有 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;
/// <summary>
/// 测试SendRequestAsync方法在请求有效时返回结果
/// </summary>

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>GeWuYou.$(AssemblyName)</PackageId>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
namespace GFramework.Godot.SourceGenerators.Abstractions;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI;
/// <summary>
/// 标记类型允许为带映射特性的导出集合生成批量注册代码。

View File

@ -1,4 +1,4 @@
namespace GFramework.Godot.SourceGenerators.Abstractions;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI;
/// <summary>
/// 标记场景根节点类型Source Generator 会生成场景行为样板代码。

View File

@ -1,4 +1,4 @@
namespace GFramework.Godot.SourceGenerators.Abstractions;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI;
/// <summary>
/// 标记 UI 页面类型Source Generator 会生成页面行为样板代码。

View File

@ -1,4 +1,4 @@
namespace GFramework.Godot.SourceGenerators.Abstractions;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI;
/// <summary>
/// 声明导出集合应当转发到哪个注册器成员及其方法。

View File

@ -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

View File

@ -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

View File

@ -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 { }

View File

@ -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",

View File

@ -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)
{

View File

@ -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";

Some files were not shown because too many files have changed in this diff Show More