Merge pull request #234 from GeWuYou/refactor/cqrs-architecture-decoupling

Refactor/cqrs architecture decoupling
This commit is contained in:
gewuyou 2026-04-16 19:53:44 +08:00 committed by GitHub
commit d8831733ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
156 changed files with 8332 additions and 1010 deletions

View File

@ -16,6 +16,8 @@ reviews:
auto_review:
enabled: true
drafts: false # draft PR 不 review
base_branches:
- refactor/cqrs-architecture-decoupling
chat:
auto_reply: true

View File

@ -5,6 +5,8 @@ on:
workflows: ["CI - Build & Test"]
types:
- completed
branches:
- main
workflow_dispatch:
concurrency:
group: auto-tag-main
@ -13,15 +15,15 @@ concurrency:
jobs:
auto-tag:
if: >
github.ref == 'refs/heads/main' &&
(
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.head_commit.message, '[release ci]')
)
||
github.event_name == 'workflow_dispatch'
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.head_commit.message, '[release ci]')
)
||
(
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main'
)
runs-on: ubuntu-latest
@ -61,4 +63,4 @@ jobs:
fi
git tag -a "$TAG" -m "Auto tag $TAG"
git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG"
git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG"

View File

@ -1,12 +1,10 @@
# CI/CD工作流配置构建和测试.NET项目
# 该工作流push到main/master分支或创建pull request时触发
# 该工作流在创建或更新面向任意分支的 pull request 时触发
name: CI - Build & Test
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
branches: [ '**' ]
permissions:
contents: read
@ -69,9 +67,9 @@ jobs:
# 扫描路径,. 表示扫描整个仓库
path: .
# 基础提交哈希,用于与当前提交进行比较
base: ${{ github.event.before }}
base: ${{ github.event.pull_request.base.sha }}
# 当前提交哈希,作为扫描的目标版本
head: ${{ github.sha }}
head: ${{ github.event.pull_request.head.sha }}
# 构建和测试 job并行执行
build-and-test:
@ -168,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

@ -4,14 +4,11 @@ name: "CodeQL"
# 触发事件配置
# 在以下情况下触发工作流:
# 1. 推送到main分支时
# 2. 针对main分支的拉取请求时
# 3. 每天凌晨2点执行一次
# 1. 针对任意分支的拉取请求时
# 2. 每天凌晨2点执行一次
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
branches: [ '**' ]
schedule:
- cron: '0 2 * * *'

View File

@ -244,6 +244,16 @@ bash scripts/validate-csharp-naming.sh
- Tracking updates MUST reflect completed work, newly discovered issues, validation results, and the next recommended
recovery point.
- Completing code changes without updating the active tracking document is considered incomplete work.
- For any multi-step refactor, migration, or cross-module task, contributors MUST create or adopt a dedicated recovery
document under `local-plan/todos/` before making substantive code changes.
- Recovery documents MUST record the current phase, the active recovery point identifier, known risks, and the next
recommended resume step so another contributor or subagent can continue the work safely.
- Contributors MUST maintain a matching execution trace under `local-plan/traces/` for complex work. The trace should
record the current date, key decisions, validation milestones, and the immediate next step.
- When a task spans multiple commits or is likely to exceed a single agent context window, update both the recovery
document and the trace at each meaningful milestone before pausing or handing work off.
- If subagents are used on a complex task, the main agent MUST capture the delegated scope and any accepted findings in
the active recovery document or trace before continuing implementation.
### Repository Documentation

View File

@ -73,7 +73,9 @@ Architecture 负责统一生命周期编排,核心阶段包括:
### CQRS
命令与查询分离支持同步与异步执行。Mediator 模式通过源码生成器集成,以减少模板代码并保持调用路径清晰。
命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史
`Mediator` 命名以兼容旧调用点,但这些别名已进入正式弃用周期:新代码应使用 `Cqrs` 命名入口,旧别名会继续兼容一段时间并计划在未来
major 版本中移除。
### EventBus
@ -103,6 +105,8 @@ Architecture 负责统一生命周期编排,核心阶段包括:
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描;非默认程序集可通过
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 显式接入同一路径。
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。

View File

@ -1,8 +1,9 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Abstractions.Architectures;
@ -73,15 +74,46 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility;
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 既支持实现 <c>IPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一请求/响应对的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
void RegisterCqrsPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// 既支持实现 <c>IPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一请求/响应对的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
void RegisterMediatorBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
void RegisterCqrsHandlersFromAssembly(Assembly assembly);
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies);
/// <summary>
/// 安装架构模块
/// </summary>
@ -101,4 +133,4 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
/// </summary>
/// <returns>表示异步等待操作的任务</returns>
Task WaitUntilReadyAsync();
}
}

View File

@ -5,15 +5,20 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Abstractions.Architectures;
/// <summary>
/// 架构上下文接口,提供对系统、模型、工具类的访问以及命令、查询、事件的发送和注册功能
/// 架构上下文接口,统一暴露框架组件访问、兼容旧命令/查询总线,以及当前推荐的 CQRS 运行时入口。
/// </summary>
public interface IArchitectureContext
/// <remarks>
/// <para>旧的 <c>GFramework.Core.Abstractions.Command</c> 与 <c>GFramework.Core.Abstractions.Query</c> 契约会继续通过原有 Command/Query Executor 路径执行,以保证存量代码兼容。</para>
/// <para>新的 <c>GFramework.Cqrs.Abstractions.Cqrs</c> 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。</para>
/// <para>新功能优先使用 <see cref="SendRequestAsync{TResponse}(IRequest{TResponse},CancellationToken)" />、<see cref="SendAsync{TCommand}(TCommand,CancellationToken)" /> 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。</para>
/// </remarks>
public interface IArchitectureContext : ICqrsContext
{
/// <summary>
/// 获取指定类型的服务实例
@ -104,87 +109,92 @@ public interface IArchitectureContext
IReadOnlyList<TUtility> GetUtilitiesByPriority<TUtility>() where TUtility : class, IUtility;
/// <summary>
/// 发送一个命令
/// 发送一个旧版命令
/// </summary>
/// <param name="command">要发送的命令</param>
/// <param name="command">要发送的旧版命令</param>
void SendCommand(ICommand command);
/// <summary>
/// 发送一个带返回值命令
/// 发送一个旧版带返回值命令
/// </summary>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
/// <param name="command">要发送的命令</param>
/// <returns>命令执行结果</returns>
TResult SendCommand<TResult>(Command.ICommand<TResult> command);
/// <typeparam name="TResult">命令执行结果类型</typeparam>
/// <param name="command">要发送的旧版命令</param>
/// <returns>命令执行结果</returns>
TResult SendCommand<TResult>(ICommand<TResult> command);
/// <summary>
/// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性)
/// 发送一个新版 CQRS 命令并返回结果。
/// </summary>
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <returns>命令执行结果</returns>
TResponse SendCommand<TResponse>(Mediator.ICommand<TResponse> command);
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的 CQRS 命令。</param>
/// <returns>命令执行结果。</returns>
/// <remarks>
/// 这是迁移后的推荐命令入口。无返回值命令应实现 <c>IRequest&lt;Unit&gt;</c>,并优先通过 <see cref="SendAsync{TCommand}(TCommand,CancellationToken)" /> 调用。
/// </remarks>
TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command);
/// <summary>
/// 发送并异步执行一个命令
/// 异步发送一个旧版命令。
/// </summary>
/// <param name="command">要发送的命令</param>
/// <param name="command">要发送的旧版命令</param>
Task SendCommandAsync(IAsyncCommand command);
/// <summary>
/// [Mediator] 异步发送命令并返回结果
/// 通过Mediator模式发送命令请求支持取消操作
/// 异步发送一个新版 CQRS 命令并返回结果。
/// </summary>
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含命令执行结果的ValueTask</returns>
ValueTask<TResponse> SendCommandAsync<TResponse>(Mediator.ICommand<TResponse> command,
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的 CQRS 命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>包含命令执行结果的值任务。</returns>
ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default);
/// <summary>
/// 发送并异步执行一个带返回值的命令
/// 异步发送一个旧版带返回值命令。
/// </summary>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
/// <param name="command">要发送的命令</param>
/// <returns>命令执行结果</returns>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
/// <param name="command">要发送的旧版命令</param>
/// <returns>命令执行结果</returns>
Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command);
/// <summary>
/// 发送一个查询请求
/// 发送一个旧版查询请求
/// </summary>
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="query">要发送的查询</param>
/// <returns>查询结果</returns>
TResult SendQuery<TResult>(Query.IQuery<TResult> query);
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="query">要发送的旧版查询</param>
/// <returns>查询结果</returns>
TResult SendQuery<TResult>(IQuery<TResult> query);
/// <summary>
/// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性)
/// 发送一个新版 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <returns>查询结果</returns>
TResponse SendQuery<TResponse>(Mediator.IQuery<TResponse> query);
/// <typeparam name="TResponse">查询响应类型。</typeparam>
/// <param name="query">要发送的 CQRS 查询。</param>
/// <returns>查询结果。</returns>
/// <remarks>
/// 这是迁移后的推荐查询入口。新查询应优先实现 <c>GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery&lt;TResponse&gt;</c>。
/// </remarks>
TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query);
/// <summary>
/// 异步发送一个查询请求
/// 异步发送一个旧版查询请求
/// </summary>
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="query">要发送的异步查询</param>
/// <returns>查询结果</returns>
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="query">要发送的旧版异步查询</param>
/// <returns>查询结果</returns>
Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query);
/// <summary>
/// [Mediator] 异步发送查询并返回结果
/// 通过Mediator模式发送查询请求支持取消操作
/// 异步发送一个新版 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含查询结果的ValueTask</returns>
ValueTask<TResponse> SendQueryAsync<TResponse>(Mediator.IQuery<TResponse> query,
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的 CQRS 查询。</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>包含查询结果的值任务。</returns>
ValueTask<TResponse> SendQueryAsync<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default);
/// <summary>
@ -216,28 +226,40 @@ public interface IArchitectureContext
void UnRegisterEvent<TEvent>(Action<TEvent> onEvent);
/// <summary>
/// 发送请求(统一处理 Command/Query
/// 发送新版 CQRS 请求,并统一处理命令与查询。
/// </summary>
/// <remarks>
/// 这是自有 CQRS 运行时的主入口。新代码应优先通过该方法或 <see cref="SendAsync{TCommand}(TCommand,CancellationToken)" /> 进入 dispatcher。
/// </remarks>
ValueTask<TResponse> SendRequestAsync<TResponse>(
IRequest<TResponse> request,
CancellationToken cancellationToken = default);
/// <summary>
/// 发送请求(同步版本,不推荐)
/// 发送新版 CQRS 请求的同步包装版本。
/// </summary>
/// <remarks>
/// 仅为兼容同步调用链保留;新代码应优先使用异步入口,避免阻塞当前线程。
/// </remarks>
TResponse SendRequest<TResponse>(IRequest<TResponse> request);
/// <summary>
/// 发布通知(一对多事件)
/// 发布新版 CQRS 通知。
/// </summary>
/// <remarks>
/// 该入口用于一对多通知分发,与框架级 <c>EventBus</c> 事件系统并存,适合围绕请求处理过程传播领域通知。
/// </remarks>
ValueTask PublishAsync<TNotification>(
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification;
/// <summary>
/// 创建流式请求(用于大数据集)
/// 创建新版 CQRS 流式请求。
/// </summary>
/// <remarks>
/// 适用于需要按序惰性产出大量结果的场景。调用方应消费返回的异步序列,而不是回退到旧版查询总线。
/// </remarks>
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default);
@ -245,7 +267,7 @@ public interface IArchitectureContext
// === 便捷扩展方法 ===
/// <summary>
/// 发送命令(无返回值)
/// 发送一个无返回值的新版 CQRS 命令。
/// </summary>
ValueTask SendAsync<TCommand>(
TCommand command,
@ -253,7 +275,7 @@ public interface IArchitectureContext
where TCommand : IRequest<Unit>;
/// <summary>
/// 发送命令(有返回值)
/// 发送一个有返回值的新版 CQRS 请求。
/// </summary>
ValueTask<TResponse> SendAsync<TResponse>(
IRequest<TResponse> command,
@ -265,4 +287,4 @@ public interface IArchitectureContext
/// </summary>
/// <returns>环境对象实例</returns>
IEnvironment GetEnvironment();
}
}

View File

@ -0,0 +1,16 @@
using System.ComponentModel;
namespace GFramework.Core.Abstractions.Cqrs;
/// <summary>
/// 提供旧 <c>GFramework.Core.Abstractions.Cqrs</c> 命名空间下的 CQRS runtime 兼容别名。
/// </summary>
/// <remarks>
/// 正式 runtime seam 已迁移到 <see cref="GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime" />
/// 但当前仍保留该接口以避免立即打断历史公开路径与既有二进制引用。
/// 新代码应优先依赖 <c>GFramework.Cqrs.Abstractions.Cqrs</c> 下的正式契约。
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface ICqrsRuntime : GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime
{
}

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>
@ -26,6 +29,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="3.0.2"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5"/>
</ItemGroup>
</Project>

View File

@ -16,4 +16,5 @@ global using System.Collections.Generic;
global using System.Runtime;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Tasks;
global using Microsoft.Extensions.DependencyInjection;

View File

@ -1,6 +1,7 @@
using GFramework.Core.Abstractions.Rule;
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Systems;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Abstractions.Ioc;
@ -90,13 +91,43 @@ public interface IIocContainer : IContextAware
void RegisterFactory<TService>(Func<IServiceProvider, TService> factory) where TService : class;
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑
/// 注册 CQRS 请求管道行为。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
void RegisterCqrsPipelineBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
void RegisterMediatorBehavior<TBehavior>()
where TBehavior : class;
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
/// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
void RegisterCqrsHandlersFromAssembly(Assembly assembly);
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies);
/// <summary>
/// 配置服务
@ -227,4 +258,4 @@ public interface IIocContainer : IContextAware
IServiceScope CreateScope();
#endregion
}
}

View File

@ -0,0 +1,281 @@
namespace GFramework.Core.Abstractions.Logging;
/// <summary>
/// 提供全局日志工厂访问入口。
/// </summary>
/// <remarks>
/// 该类型位于抽象层,是为了让上层模块可以在不依赖 <c>GFramework.Core</c> 实现程序集的前提下
/// 获取日志记录器。默认 provider 会优先通过反射解析 <c>GFramework.Core</c> 中的控制台实现,
/// 若宿主未加载该程序集,则退回到静默 provider避免抽象层形成实现层循环依赖。
/// </remarks>
public static class LoggerFactoryResolver
{
private static readonly object ProviderLock = new();
private static string DefaultProviderTypeName =
"GFramework.Core.Logging.ConsoleLoggerFactoryProvider, GFramework.Core";
private static ILoggerFactoryProvider? _provider;
/// <summary>
/// 获取或设置当前日志工厂提供程序。
/// </summary>
/// <remarks>
/// 读取与赋值都会通过同一把锁串行化,确保并发调用方观察到确定的 provider 引用。
/// 当调用方未显式赋值时,会在首次访问时尝试解析默认实现;若解析失败,则退回静默 provider。
/// </remarks>
/// <exception cref="ArgumentNullException">
/// 当赋值为 <see langword="null" /> 时抛出。
/// </exception>
public static ILoggerFactoryProvider Provider
{
get
{
lock (ProviderLock)
{
_provider ??= CreateDefaultProvider();
return _provider;
}
}
set
{
var provider = value ?? throw new ArgumentNullException(nameof(value));
lock (ProviderLock)
{
_provider = provider;
}
}
}
/// <summary>
/// 获取或设置新创建日志记录器的最小日志级别。
/// </summary>
/// <remarks>
/// 该属性直接代理到当前 <see cref="Provider" />,确保调用方调整级别后立即影响后续创建的日志器。
/// </remarks>
public static LogLevel MinLevel
{
get => Provider.MinLevel;
set => Provider.MinLevel = value;
}
private static ILoggerFactoryProvider CreateDefaultProvider()
{
try
{
if (Type.GetType(DefaultProviderTypeName, throwOnError: false) is { } providerType &&
Activator.CreateInstance(providerType) is ILoggerFactoryProvider provider)
{
provider.MinLevel = LogLevel.Info;
return provider;
}
}
catch (Exception)
{
// The default provider is optional. Any load or activation failure must degrade to the silent provider so
// abstractions-only hosts can continue bootstrapping without the concrete logging assembly.
}
return new SilentLoggerFactoryProvider();
}
/// <summary>
/// 当宿主未提供默认日志实现时使用的静默 provider。
/// </summary>
private sealed class SilentLoggerFactoryProvider : ILoggerFactoryProvider
{
public LogLevel MinLevel { get; set; } = LogLevel.Info;
public ILogger CreateLogger(string name)
{
return new SilentLogger(name);
}
}
/// <summary>
/// 默认日志实现不可用时的 no-op 日志器。
/// </summary>
private sealed class SilentLogger(string name) : ILogger
{
public string Name()
{
return name;
}
public bool IsTraceEnabled()
{
return false;
}
public bool IsDebugEnabled()
{
return false;
}
public bool IsInfoEnabled()
{
return false;
}
public bool IsWarnEnabled()
{
return false;
}
public bool IsErrorEnabled()
{
return false;
}
public bool IsFatalEnabled()
{
return false;
}
public bool IsEnabledForLevel(LogLevel level)
{
return false;
}
public void Trace(string msg)
{
}
public void Trace(string format, object arg)
{
}
public void Trace(string format, object arg1, object arg2)
{
}
public void Trace(string format, params object[] arguments)
{
}
public void Trace(string msg, Exception t)
{
}
public void Debug(string msg)
{
}
public void Debug(string format, object arg)
{
}
public void Debug(string format, object arg1, object arg2)
{
}
public void Debug(string format, params object[] arguments)
{
}
public void Debug(string msg, Exception t)
{
}
public void Info(string msg)
{
}
public void Info(string format, object arg)
{
}
public void Info(string format, object arg1, object arg2)
{
}
public void Info(string format, params object[] arguments)
{
}
public void Info(string msg, Exception t)
{
}
public void Warn(string msg)
{
}
public void Warn(string format, object arg)
{
}
public void Warn(string format, object arg1, object arg2)
{
}
public void Warn(string format, params object[] arguments)
{
}
public void Warn(string msg, Exception t)
{
}
public void Error(string msg)
{
}
public void Error(string format, object arg)
{
}
public void Error(string format, object arg1, object arg2)
{
}
public void Error(string format, params object[] arguments)
{
}
public void Error(string msg, Exception t)
{
}
public void Fatal(string msg)
{
}
public void Fatal(string format, object arg)
{
}
public void Fatal(string format, object arg1, object arg2)
{
}
public void Fatal(string format, params object[] arguments)
{
}
public void Fatal(string msg, Exception t)
{
}
public void Log(LogLevel level, string message)
{
}
public void Log(LogLevel level, string format, object arg)
{
}
public void Log(LogLevel level, string format, object arg1, object arg2)
{
}
public void Log(LogLevel level, string format, params object[] arguments)
{
}
public void Log(LogLevel level, string message, Exception exception)
{
}
}
}

View File

@ -0,0 +1,201 @@
using System.Reflection;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证架构初始化阶段可以显式接入默认程序集之外的 CQRS handlers。
/// </summary>
[TestFixture]
public sealed class ArchitectureAdditionalCqrsHandlersTests
{
/// <summary>
/// 初始化日志工厂和共享测试状态。
/// </summary>
[SetUp]
public void SetUp()
{
_previousLoggerFactoryProvider = LoggerFactoryResolver.Provider;
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
AdditionalAssemblyNotificationHandlerState.Reset();
}
/// <summary>
/// 清理测试过程中写入的共享状态。
/// </summary>
[TearDown]
public void TearDown()
{
AdditionalAssemblyNotificationHandlerState.Reset();
GameContext.Clear();
LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider
?? throw new InvalidOperationException(
"LoggerFactoryResolver.Provider should be captured during setup.");
}
private ILoggerFactoryProvider? _previousLoggerFactoryProvider;
/// <summary>
/// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。
/// </summary>
/// <returns>The asynchronous test task.</returns>
[Test]
public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly()
{
var generatedAssembly = CreateGeneratedHandlerAssembly();
var architecture = CreateArchitecture(target =>
target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object));
await architecture.InitializeAsync();
try
{
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 验证不同 <see cref="Assembly" /> 实例只要解析到相同程序集键,就不会向容器重复写入相同 handler 映射。
/// </summary>
/// <returns>The asynchronous test task.</returns>
[Test]
public async Task RegisterCqrsHandlersFromAssembly_Should_Deduplicate_Repeated_Assembly_Registration()
{
var generatedAssemblyA = CreateGeneratedHandlerAssembly();
var generatedAssemblyB = CreateGeneratedHandlerAssembly();
var architecture = CreateArchitecture(target =>
{
target.RegisterCqrsHandlersFromAssembly(generatedAssemblyA.Object);
target.RegisterCqrsHandlersFromAssemblies([generatedAssemblyB.Object]);
});
await architecture.InitializeAsync();
try
{
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
}
finally
{
await architecture.DestroyAsync();
}
}
/// <summary>
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
/// 该测试替身模拟“扩展程序集已经挂接 source-generator运行时只需显式接入该程序集”的真实路径。
/// </summary>
/// <returns>包含程序集级 handler registry 元数据的 mocked Assembly。</returns>
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
return generatedAssembly;
}
/// <summary>
/// 创建复用现有测试架构基建的测试架构,并在注册阶段后执行额外程序集接入逻辑。
/// </summary>
/// <param name="configure">初始化阶段执行的额外 CQRS 程序集接入逻辑。</param>
/// <returns>带有注册后钩子的测试架构实例。</returns>
private static SyncTestArchitecture CreateArchitecture(Action<TestArchitectureBase> configure)
{
var architecture = new SyncTestArchitecture();
architecture.AddPostRegistrationHook(configure);
return architecture;
}
}
/// <summary>
/// 用于验证额外程序集接入是否成功的测试通知。
/// </summary>
public sealed record AdditionalAssemblyNotification : INotification;
/// <summary>
/// 记录模拟扩展程序集通知处理器的执行次数。
/// </summary>
public static class AdditionalAssemblyNotificationHandlerState
{
private static int _invocationCount;
/// <summary>
/// 获取当前测试进程中该处理器的执行次数。
/// </summary>
/// <remarks>
/// 该计数器通过原子读写维护,以支持 NUnit 并行执行环境中的并发访问。
/// </remarks>
public static int InvocationCount => Volatile.Read(ref _invocationCount);
/// <summary>
/// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。
/// </summary>
public static void RecordInvocation()
{
Interlocked.Increment(ref _invocationCount);
}
/// <summary>
/// 清理共享计数器,避免测试间相互污染。
/// </summary>
public static void Reset()
{
Interlocked.Exchange(ref _invocationCount, 0);
}
}
/// <summary>
/// 模拟由 source-generator 为扩展程序集生成的 CQRS handler registry。
/// </summary>
internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将扩展程序集中的通知处理器映射写入服务集合。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="logger">日志记录器。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="services" /> 或 <paramref name="logger" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient<INotificationHandler<AdditionalAssemblyNotification>>(_ => CreateHandler());
logger.Debug(
$"Registered CQRS handler proxy for {typeof(INotificationHandler<AdditionalAssemblyNotification>).FullName}.");
}
/// <summary>
/// 创建一个仅供显式程序集注册路径使用的动态通知处理器。
/// </summary>
/// <returns>用于记录通知触发次数的测试替身处理器。</returns>
private static INotificationHandler<AdditionalAssemblyNotification> CreateHandler()
{
var handler = new Mock<INotificationHandler<AdditionalAssemblyNotification>>();
handler
.Setup(target => target.Handle(It.IsAny<AdditionalAssemblyNotification>(), It.IsAny<CancellationToken>()))
.Returns(() =>
{
AdditionalAssemblyNotificationHandlerState.RecordInvocation();
return ValueTask.CompletedTask;
});
return handler.Object;
}
}

View File

@ -1,11 +1,11 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@ -714,4 +714,4 @@ public class ArchitectureComponentRegistryBehaviorTests
return _context;
}
}
}
}

View File

@ -3,6 +3,8 @@ using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Environment;
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;
@ -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

@ -1,9 +1,9 @@
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Environment;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@ -185,4 +185,4 @@ public class ArchitectureInitializationPipelineTests
private sealed class BootstrapMarker
{
}
}
}

View File

@ -2,12 +2,12 @@ using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@ -460,4 +460,4 @@ public class ArchitectureLifecycleBehaviorTests
return _context;
}
}
}
}

View File

@ -1,15 +1,15 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 Mediator 行为注册能力。
/// 这些测试覆盖模块安装回调和中介管道行为接入,确保模块管理器仍然保持可观察行为不变。
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 CQRS 行为注册能力。
/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。
/// </summary>
[TestFixture]
public class ArchitectureModulesBehaviorTests
@ -57,7 +57,29 @@ public class ArchitectureModulesBehaviorTests
}
/// <summary>
/// 验证注册的 Mediator 行为会参与请求管道执行。
/// 验证注册的 CQRS 行为会参与请求管道执行。
/// </summary>
[Test]
public async Task RegisterCqrsPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Request()
{
var architecture = new ModuleTestArchitecture(target =>
target.RegisterCqrsPipelineBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
await architecture.InitializeAsync();
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo("handled"));
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证兼容别名 <c>RegisterMediatorBehavior</c> 仍会把 CQRS 行为接入请求管道。
/// </summary>
[Test]
public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request()
@ -83,12 +105,6 @@ public class ArchitectureModulesBehaviorTests
/// </summary>
private sealed class ModuleTestArchitecture(Action<ModuleTestArchitecture> registrationAction) : Architecture
{
/// <summary>
/// 打开 Mediator 服务注册,以便测试中介行为接入。
/// </summary>
public override Action<IServiceCollection>? Configurator =>
services => services.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; });
/// <summary>
/// 在初始化阶段执行测试注入的模块注册逻辑。
/// </summary>
@ -178,11 +194,10 @@ public sealed class TrackingPipelineBehavior<TRequest, TResponse> : IPipelineBeh
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理器的响应结果。</returns>
public async ValueTask<TResponse> Handle(
TRequest message,
MessageHandlerDelegate<TRequest, TResponse> next,
TRequest message, MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken)
{
InvocationCount++;
return await next(message, cancellationToken);
}
}
}

View File

@ -13,7 +13,7 @@ using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Query;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Tests.Architectures;
@ -41,9 +41,10 @@ public class ArchitectureServicesTests
_context = new TestArchitectureContextV3();
}
private ArchitectureServices? _services;
private TestArchitectureContextV3? _context;
private ArchitectureServices? _services;
private void RegisterBuiltInServices()
{
_services!.ModuleManager.RegisterBuiltInModules(_services.Container);
@ -358,24 +359,56 @@ public class TestArchitectureContextV3 : IArchitectureContext
throw new NotImplementedException();
}
public ValueTask<TResponse> SendCommandAsync<TResponse>(global::Mediator.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>(global::Mediator.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>(global::Mediator.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>(global::Mediator.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();
}
@ -386,7 +419,8 @@ public class TestArchitectureContextV3 : IArchitectureContext
throw new NotImplementedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request,
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
@ -439,4 +473,4 @@ public class TestArchitectureContextV3 : IArchitectureContext
}
}
#endregion
#endregion

View File

@ -13,7 +13,7 @@ using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Query;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Tests.Architectures;
@ -394,57 +394,137 @@ public class TestArchitectureContext : IArchitectureContext
{
}
/// <summary>
/// 测试桩:异步发送统一 CQRS 请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试桩:同步发送统一 CQRS 请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <returns>请求响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> SendCommandAsync<TResponse>(global::Mediator.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>(global::Mediator.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>(global::Mediator.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>(global::Mediator.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();
}
/// <summary>
/// 测试桩:异步发布 CQRS 通知。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>通知发布任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request,
/// <summary>
/// 测试桩:创建 CQRS 流式请求响应序列。
/// </summary>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应流。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试桩:异步发送无返回值 CQRS 命令。
/// </summary>
/// <typeparam name="TCommand">命令类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令发送任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
}
/// <summary>
/// 测试桩:异步发送带返回值的 CQRS 请求。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="command">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
@ -465,7 +545,7 @@ public class TestArchitectureContext : 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)
{
return default!;
}
@ -486,7 +566,7 @@ public class TestArchitectureContext : 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)
{
return default!;
}
@ -510,4 +590,4 @@ public class TestArchitectureContext : IArchitectureContext
{
return Environment;
}
}
}

View File

@ -1,5 +1,6 @@
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
@ -244,4 +245,4 @@ public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized
public int Priority => 30;
}
#endregion
#endregion

View File

@ -1,3 +1,4 @@
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
@ -5,7 +6,6 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
@ -181,11 +181,37 @@ public class TestArchitectureWithRegistry : IArchitecture
throw new NotImplementedException();
}
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
}
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
RegisterCqrsPipelineBehavior<TBehavior>();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
@ -306,11 +332,37 @@ public class TestArchitectureWithoutRegistry : IArchitecture
throw new NotImplementedException();
}
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
}
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
RegisterCqrsPipelineBehavior<TBehavior>();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
@ -363,4 +415,4 @@ public class TestArchitectureWithoutRegistry : IArchitecture
public void RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
}
}
}

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

@ -1,104 +0,0 @@
// 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.Architectures;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine.Extensions;
using Mediator;
using Moq;
namespace GFramework.Core.Tests.Coroutine;
/// <summary>
/// MediatorCoroutineExtensions的单元测试类
/// 测试Mediator模式与协程集成的扩展方法
/// 注意:由于 Mediator 使用源生成器,本测试类主要验证接口和参数验证
/// </summary>
[TestFixture]
public class MediatorCoroutineExtensionsTests
{
/// <summary>
/// 测试用的简单命令类
/// </summary>
private class TestCommand : IRequest<Unit>
{
public string Data { get; set; } = string.Empty;
}
/// <summary>
/// 测试用的简单事件类
/// </summary>
private class TestEvent
{
public string Data { get; set; } = string.Empty;
}
/// <summary>
/// 上下文感知基类的模拟实现
/// </summary>
private class TestContextAware : IContextAware
{
public readonly Mock<IArchitectureContext> _mockContext = new();
public IArchitectureContext GetContext()
{
return _mockContext.Object;
}
public void SetContext(IArchitectureContext context)
{
}
}
/// <summary>
/// 验证SendCommandCoroutine应该返回IEnumerator<IYieldInstruction>
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Return_IEnumerator_Of_YieldInstruction()
{
var command = new TestCommand { Data = "Test" };
var contextAware = new TestContextAware();
// 创建 mediator 模拟
var mediatorMock = new Mock<IMediator>();
contextAware._mockContext
.Setup(ctx => ctx.GetService<IMediator>())
.Returns(mediatorMock.Object);
var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine, Is.InstanceOf<IEnumerator<IYieldInstruction>>());
}
/// <summary>
/// 验证SendCommandCoroutine应该在mediator为null时抛出NullReferenceException
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Throw_When_Mediator_Null()
{
var command = new TestCommand { Data = "Test" };
var contextAware = new TestContextAware();
// 设置上下文服务以返回null mediator
contextAware._mockContext
.Setup(ctx => ctx.GetService<IMediator>())
.Returns((IMediator?)null);
// 创建协程
var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command);
// 调用 MoveNext 时应该抛出 NullReferenceException
Assert.Throws<NullReferenceException>(() => coroutine.MoveNext());
}
}

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

@ -0,0 +1,76 @@
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using GFramework.Cqrs.Abstractions.Cqrs.Notification;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
using GFramework.Cqrs.Abstractions.Cqrs.Request;
using GFramework.Cqrs.Command;
using GFramework.Cqrs.Notification;
using GFramework.Cqrs.Query;
using GFramework.Cqrs.Request;
namespace GFramework.Core.Tests.Cqrs;
/// <summary>
/// 锁定 CQRS 基础消息类型在 runtime 拆分后的公开命名空间与程序集兼容性。
/// </summary>
[TestFixture]
public sealed class CqrsPublicNamespaceCompatibilityTests
{
/// <summary>
/// 验证基础消息类型继续暴露在历史公开 CQRS 命名空间GFramework.Cqrs.*),同时由独立 runtime 程序集承载实现。
/// </summary>
[Test]
public void Base_Message_Types_Should_Live_In_Cqrs_Namespaces_And_Runtime_Assembly()
{
Assert.Multiple(() =>
{
AssertLegacyType(typeof(CommandBase<TestCommandInput, Unit>), "GFramework.Cqrs.Command");
AssertLegacyType(typeof(QueryBase<TestQueryInput, string>), "GFramework.Cqrs.Query");
AssertLegacyType(typeof(RequestBase<TestRequestInput, string>), "GFramework.Cqrs.Request");
AssertLegacyType(typeof(NotificationBase<TestNotificationInput>), "GFramework.Cqrs.Notification");
});
}
/// <summary>
/// 验证旧的 GFramework.Core 程序集限定名仍可解析到迁移后的 runtime 实现类型。
/// </summary>
[Test]
public void Type_Forwarding_Should_Resolve_Cqrs_Types_From_Core_Assembly()
{
Assert.Multiple(() =>
{
AssertForwardedType("GFramework.Cqrs.Command.CommandBase`2, GFramework.Core");
AssertForwardedType("GFramework.Cqrs.Query.QueryBase`2, GFramework.Core");
AssertForwardedType("GFramework.Cqrs.Request.RequestBase`2, GFramework.Core");
AssertForwardedType("GFramework.Cqrs.Notification.NotificationBase`1, GFramework.Core");
});
}
private static void AssertLegacyType(Type type, string expectedNamespace)
{
Assert.Multiple(() =>
{
Assert.That(type.Namespace, Is.EqualTo(expectedNamespace));
Assert.That(type.Assembly.GetName().Name, Is.EqualTo("GFramework.Cqrs"));
});
}
private static void AssertForwardedType(string assemblyQualifiedTypeName)
{
var resolvedType = Type.GetType(assemblyQualifiedTypeName, throwOnError: false);
Assert.Multiple(() =>
{
Assert.That(resolvedType, Is.Not.Null);
Assert.That(resolvedType!.Assembly.GetName().Name, Is.EqualTo("GFramework.Cqrs"));
});
}
private sealed record TestCommandInput : ICommandInput;
private sealed record TestQueryInput : IQueryInput;
private sealed record TestRequestInput : IRequestInput;
private sealed record TestNotificationInput : INotificationInput;
}

View File

@ -0,0 +1,99 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Architectures;
using GFramework.Core.Coroutine.Extensions;
using GFramework.Core.Ioc;
namespace GFramework.Core.Tests.Cqrs;
/// <summary>
/// 锁定历史 Mediator 兼容入口的正式弃用策略。
/// 这些测试确保旧 API 不仅保留行为兼容,还会通过编译期提示和 IntelliSense 隐藏引导调用方迁移到新的 CQRS 命名。
/// </summary>
[TestFixture]
public class MediatorCompatibilityDeprecationTests
{
/// <summary>
/// 验证公开兼容方法仍可用,但已被显式标记为未来移除的旧别名。
/// </summary>
[Test]
public void Legacy_Public_Methods_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing()
{
AssertLegacyMethod(typeof(IArchitecture), nameof(IArchitecture.RegisterMediatorBehavior));
AssertLegacyMethod(typeof(IIocContainer), nameof(IIocContainer.RegisterMediatorBehavior));
AssertLegacyMethod(typeof(Architecture), nameof(Architecture.RegisterMediatorBehavior));
AssertLegacyMethod(typeof(MicrosoftDiContainer), nameof(MicrosoftDiContainer.RegisterMediatorBehavior));
}
/// <summary>
/// 验证历史扩展类型会把迁移目标写入弃用说明,并从 IntelliSense 主路径隐藏。
/// </summary>
[Test]
public void Legacy_Extension_Types_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing()
{
AssertLegacyType(
typeof(ContextAwareMediatorExtensions),
"Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead.");
AssertLegacyType(
typeof(ContextAwareMediatorCommandExtensions),
"Use GFramework.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.");
AssertLegacyType(
typeof(ContextAwareMediatorQueryExtensions),
"Use GFramework.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.");
AssertLegacyType(
typeof(MediatorCoroutineExtensions),
"Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead.");
}
/// <summary>
/// 断言方法级兼容 API 具备统一的弃用元数据。
/// </summary>
/// <param name="declaringType">声明该方法的类型。</param>
/// <param name="methodName">方法名称。</param>
private static void AssertLegacyMethod(Type declaringType, string methodName)
{
var method = declaringType
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Single(candidate => candidate.Name == methodName);
var obsoleteAttribute = method.GetCustomAttribute<ObsoleteAttribute>();
var editorBrowsableAttribute = method.GetCustomAttribute<EditorBrowsableAttribute>();
Assert.Multiple(() =>
{
Assert.That(obsoleteAttribute, Is.Not.Null);
Assert.That(
obsoleteAttribute!.Message,
Does.Contain("Use RegisterCqrsPipelineBehavior<TBehavior>() instead."));
Assert.That(
obsoleteAttribute.Message,
Does.Contain("removed in a future major version"));
Assert.That(editorBrowsableAttribute, Is.Not.Null);
Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never));
});
}
/// <summary>
/// 断言类型级兼容扩展具备统一的弃用元数据。
/// </summary>
/// <param name="type">兼容扩展类型。</param>
/// <param name="expectedReplacementHint">期望的迁移提示。</param>
private static void AssertLegacyType(Type type, string expectedReplacementHint)
{
var obsoleteAttribute = type.GetCustomAttribute<ObsoleteAttribute>();
var editorBrowsableAttribute = type.GetCustomAttribute<EditorBrowsableAttribute>();
Assert.Multiple(() =>
{
Assert.That(obsoleteAttribute, Is.Not.Null);
Assert.That(obsoleteAttribute!.Message, Does.Contain(expectedReplacementHint));
Assert.That(
obsoleteAttribute.Message,
Does.Contain("removed in a future major version"));
Assert.That(editorBrowsableAttribute, Is.Not.Null);
Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never));
});
}
}

View File

@ -10,11 +10,6 @@
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mediator.Abstractions" Version="3.0.2"/>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>
<PackageReference Include="Moq" Version="4.20.72"/>
<PackageReference Include="NUnit" Version="4.5.1"/>
@ -23,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;
@ -23,4 +24,6 @@ global using GFramework.Core.Abstractions.StateManagement;
global using GFramework.Core.Extensions;
global using GFramework.Core.Property;
global using GFramework.Core.StateManagement;
global using GFramework.Core.Abstractions.Property;
global using GFramework.Core.Abstractions.Property;
global using Microsoft.Extensions.DependencyInjection;
global using Moq;

View File

@ -1,8 +1,12 @@
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Tests.Cqrs;
using GFramework.Core.Tests.Systems;
using GFramework.Cqrs.Abstractions.Cqrs;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
namespace GFramework.Core.Tests.Ioc;
@ -27,6 +31,8 @@ public class MicrosoftDiContainerTests
BindingFlags.NonPublic | BindingFlags.Instance);
loggerField?.SetValue(_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(MicrosoftDiContainer)));
CqrsTestRuntime.RegisterInfrastructure(_container);
}
private MicrosoftDiContainer _container = null!;
@ -147,6 +153,25 @@ 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<LegacyICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
Assert.That(_container.Get<ICqrsRuntime>(), Is.SameAs(_container.Get<LegacyICqrsRuntime>()));
CqrsTestRuntime.RegisterHandlers(_container);
Assert.That(_container.GetAll<ICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<LegacyICqrsRuntime>(), Has.Count.EqualTo(1));
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
Assert.That(_container.Get<ICqrsRuntime>(), Is.SameAs(_container.Get<LegacyICqrsRuntime>()));
}
/// <summary>
/// 测试当没有实例时获取应返回 null 的功能
/// </summary>
@ -224,6 +249,46 @@ public class MicrosoftDiContainerTests
Assert.That(results.Count, Is.EqualTo(0));
}
/// <summary>
/// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复,
/// 但会保留同一服务类型的重复显式注册。
/// </summary>
[Test]
public void GetAll_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases()
{
var instance = new AliasAwareService();
_container.Register<IPrimaryAliasService>(instance);
_container.Register<IPrimaryAliasService>(instance);
_container.Register<ISecondaryAliasService>(instance);
var results = _container.GetAll<ISharedAliasService>();
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results[0], Is.SameAs(instance));
Assert.That(results[1], Is.SameAs(instance));
}
/// <summary>
/// 测试非泛型 GetAll 在容器未冻结时与泛型重载保持相同的别名去重语义。
/// </summary>
[Test]
public void
GetAll_Type_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases()
{
var instance = new AliasAwareService();
_container.Register<IPrimaryAliasService>(instance);
_container.Register<IPrimaryAliasService>(instance);
_container.Register<ISecondaryAliasService>(instance);
var results = _container.GetAll(typeof(ISharedAliasService));
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results[0], Is.SameAs(instance));
Assert.That(results[1], Is.SameAs(instance));
}
/// <summary>
/// 测试获取排序后的所有实例的功能
/// </summary>
@ -306,6 +371,47 @@ public class MicrosoftDiContainerTests
Assert.That(_container.Contains<TestService>(), Is.False);
}
/// <summary>
/// 测试清空容器后可以重新接入同一程序集中的 CQRS 处理器。
/// </summary>
[Test]
public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State()
{
var assembly = typeof(DeterministicOrderNotification).Assembly;
_container.RegisterCqrsHandlersFromAssembly(assembly);
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.True);
_container.Clear();
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.False);
// Clear 会移除测试手工补齐的 CQRS seam需要先恢复基础设施再验证程序集去重状态是否已重置。
CqrsTestRuntime.RegisterInfrastructure(_container);
_container.RegisterCqrsHandlersFromAssembly(assembly);
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.True);
}
/// <summary>
/// 测试当程序集集合中包含空元素时CQRS handler 注册入口会在委托给注册服务前直接失败。
/// </summary>
[Test]
public void RegisterCqrsHandlersFromAssemblies_WithNullAssemblyItem_Should_ThrowArgumentNullException()
{
var assemblies = new Assembly[] { typeof(DeterministicOrderNotification).Assembly, null! };
Assert.Throws<ArgumentNullException>(() => _container.RegisterCqrsHandlersFromAssemblies(assemblies));
}
/// <summary>
/// 测试冻结容器以防止进一步注册的功能
/// </summary>
@ -661,6 +767,28 @@ public interface IMixedService
string? Name { get; set; }
}
/// <summary>
/// 用于验证未冻结查询路径中的服务别名去重行为。
/// </summary>
public interface ISharedAliasService;
/// <summary>
/// 主服务别名接口。
/// </summary>
public interface IPrimaryAliasService : ISharedAliasService;
/// <summary>
/// 次级兼容别名接口。
/// </summary>
public interface ISecondaryAliasService : ISharedAliasService;
/// <summary>
/// 同时实现多个别名接口的测试服务。
/// </summary>
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
{
}
/// <summary>
/// 实现优先级的服务
/// </summary>
@ -676,4 +804,4 @@ public sealed class PrioritizedService : IPrioritizedService, IMixedService
public sealed class NonPrioritizedService : IMixedService
{
public string? Name { get; set; }
}
}

View File

@ -1,18 +1,19 @@
using System.IO;
using System.Reflection;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
using NUnit.Framework;
namespace GFramework.Core.Tests.Logging;
/// <summary>
/// 测试LoggerFactory相关功能的测试类
/// 测试 LoggerFactory 相关功能的测试类
/// </summary>
[TestFixture]
[NonParallelizable]
public class LoggerFactoryTests
{
/// <summary>
/// 测试ConsoleLoggerFactory的GetLogger方法是否返回ConsoleLogger实例
/// 测试 ConsoleLoggerFactory GetLogger 方法是否返回 ConsoleLogger 实例
/// </summary>
[Test]
public void ConsoleLoggerFactory_GetLogger_ShouldReturnConsoleLogger()
@ -26,7 +27,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactory使用不同名称获取不同的logger实例
/// 测试 ConsoleLoggerFactory 使用不同名称获取不同的 logger 实例
/// </summary>
[Test]
public void ConsoleLoggerFactory_GetLogger_WithDifferentNames_ShouldReturnDifferentLoggers()
@ -40,7 +41,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactory使用默认最小级别时的行为默认为Info级别
/// 测试 ConsoleLoggerFactory 使用默认最小级别时的行为。
/// </summary>
[Test]
public void ConsoleLoggerFactory_GetLogger_WithDefaultMinLevel_ShouldUseInfo()
@ -51,7 +52,6 @@ public class LoggerFactoryTests
var stringWriter = new StringWriter();
var testLogger = new ConsoleLogger("TestLogger", LogLevel.Info, stringWriter, false);
// 验证Debug消息不会被记录但Info消息会被记录
testLogger.Debug("Debug message");
testLogger.Info("Info message");
@ -61,7 +61,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider创建logger时使用提供者的最小级别设置
/// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供者的最小级别设置
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldReturnLoggerWithProviderMinLevel()
@ -72,7 +72,6 @@ public class LoggerFactoryTests
var stringWriter = new StringWriter();
var testLogger = new ConsoleLogger("TestLogger", LogLevel.Debug, stringWriter, false);
// 验证Debug消息会被记录但Trace消息不会被记录
testLogger.Debug("Debug message");
testLogger.Trace("Trace message");
@ -82,7 +81,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider创建logger时使用提供的名称
/// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供的名称
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldUseProvidedName()
@ -94,7 +93,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的Provider属性是否有默认值
/// 测试 LoggerFactoryResolver Provider 属性是否有默认值
/// </summary>
[Test]
public void LoggerFactoryResolver_Provider_ShouldHaveDefaultValue()
@ -104,7 +103,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的Provider属性可以被更改
/// 测试 LoggerFactoryResolver Provider 属性可以被更改
/// </summary>
[Test]
public void LoggerFactoryResolver_Provider_CanBeChanged()
@ -120,7 +119,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的MinLevel属性是否有默认值
/// 测试 LoggerFactoryResolver MinLevel 属性是否有默认值
/// </summary>
[Test]
public void LoggerFactoryResolver_MinLevel_ShouldHaveDefaultValue()
@ -129,7 +128,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的MinLevel属性可以被更改
/// 测试 LoggerFactoryResolver MinLevel 属性可以被更改
/// </summary>
[Test]
public void LoggerFactoryResolver_MinLevel_CanBeChanged()
@ -144,7 +143,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider的MinLevel属性是否有默认值
/// 测试 ConsoleLoggerFactoryProvider MinLevel 属性是否有默认值
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_MinLevel_ShouldHaveDefaultValue()
@ -155,7 +154,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider的MinLevel属性可以被更改
/// 测试 ConsoleLoggerFactoryProvider MinLevel 属性可以被更改
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_MinLevel_CanBeChanged()
@ -168,7 +167,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的Provider创建logger时使用提供者设置
/// 测试 LoggerFactoryResolver Provider 创建 logger 时使用提供者设置
/// </summary>
[Test]
public void LoggerFactoryResolver_Provider_CreateLogger_ShouldUseProviderSettings()
@ -183,7 +182,6 @@ public class LoggerFactoryTests
var stringWriter = new StringWriter();
var testLogger = new ConsoleLogger("TestLogger", LogLevel.Warning, stringWriter, false);
// 验证Warn消息会被记录但Info消息不会被记录
testLogger.Warn("Warn message");
testLogger.Info("Info message");
@ -195,7 +193,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试LoggerFactoryResolver的MinLevel属性影响新创建的logger
/// 测试 LoggerFactoryResolver MinLevel 属性影响新创建的 logger
/// </summary>
[Test]
public void LoggerFactoryResolver_MinLevel_AffectsNewLoggers()
@ -210,7 +208,6 @@ public class LoggerFactoryTests
var stringWriter = new StringWriter();
var testLogger = new ConsoleLogger("TestLogger", LogLevel.Error, stringWriter, false);
// 验证Error消息会被记录但Warn消息不会被记录
testLogger.Error("Error message");
testLogger.Warn("Warn message");
@ -222,7 +219,93 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactory创建的多个logger实例是独立的
/// 验证默认 provider 激活失败时会回退到静默 provider。
/// </summary>
[Test]
public void
LoggerFactoryResolver_Provider_Should_Fall_Back_To_SilentProvider_When_DefaultProvider_Activation_Fails()
{
var originalProvider = LoggerFactoryResolver.Provider;
var originalTypeName = GetDefaultProviderTypeName();
try
{
ResetProvider();
SetDefaultProviderTypeName(typeof(ThrowingLoggerFactoryProvider).AssemblyQualifiedName!);
var provider = LoggerFactoryResolver.Provider;
var logger = provider.CreateLogger("Fallback");
Assert.Multiple(() =>
{
Assert.That(provider.GetType().Name, Is.EqualTo("SilentLoggerFactoryProvider"));
Assert.That(provider.MinLevel, Is.EqualTo(LogLevel.Info));
Assert.That(logger.IsEnabledForLevel(LogLevel.Error), Is.False);
});
}
finally
{
SetDefaultProviderTypeName(originalTypeName);
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 验证并发首次访问默认 provider 时只会创建一个实例,并向所有调用方返回相同引用。
/// </summary>
[Test]
public async Task
LoggerFactoryResolver_Provider_Should_Create_A_Single_Default_Instance_When_Accessed_Concurrently()
{
var originalProvider = LoggerFactoryResolver.Provider;
var originalTypeName = GetDefaultProviderTypeName();
try
{
BlockingLoggerFactoryProvider.Reset();
ResetProvider();
SetDefaultProviderTypeName(typeof(BlockingLoggerFactoryProvider).AssemblyQualifiedName!);
var startGate = new ManualResetEventSlim(false);
var tasks = Enumerable.Range(0, 8)
.Select(_ => Task.Run(() =>
{
startGate.Wait();
return LoggerFactoryResolver.Provider;
}))
.ToArray();
startGate.Set();
Assert.That(
SpinWait.SpinUntil(
() => BlockingLoggerFactoryProvider.ConstructionCount >= 1,
TimeSpan.FromSeconds(2)),
Is.True,
"The test provider should start construction after concurrent access begins.");
BlockingLoggerFactoryProvider.ReleaseConstruction();
var providers = await Task.WhenAll(tasks);
Assert.Multiple(() =>
{
Assert.That(BlockingLoggerFactoryProvider.ConstructionCount, Is.EqualTo(1));
Assert.That(providers.Distinct().Count(), Is.EqualTo(1));
Assert.That(LoggerFactoryResolver.Provider, Is.SameAs(providers[0]));
});
}
finally
{
BlockingLoggerFactoryProvider.ReleaseConstruction();
BlockingLoggerFactoryProvider.Reset();
SetDefaultProviderTypeName(originalTypeName);
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 测试 ConsoleLoggerFactory 创建的多个 logger 实例是独立的。
/// </summary>
[Test]
public void ConsoleLoggerFactory_MultipleLoggers_ShouldBeIndependent()
@ -236,7 +319,7 @@ public class LoggerFactoryTests
}
/// <summary>
/// 测试ConsoleLoggerFactoryProvider的MinLevel不会影响已创建的logger
/// 测试 ConsoleLoggerFactoryProvider MinLevel 不会影响已创建的 logger
/// </summary>
[Test]
public void ConsoleLoggerFactoryProvider_MinLevel_DoesNotAffectCreatedLogger()
@ -247,7 +330,6 @@ public class LoggerFactoryTests
var stringWriter = new StringWriter();
var testLogger = new ConsoleLogger("TestLogger", LogLevel.Error, stringWriter, false);
// 验证Error和Fatal消息都会被记录
testLogger.Error("Error message");
testLogger.Fatal("Fatal message");
@ -255,4 +337,114 @@ public class LoggerFactoryTests
Assert.That(output, Does.Contain("Error message"));
Assert.That(output, Does.Contain("Fatal message"));
}
}
private static string GetDefaultProviderTypeName()
{
return (string)GetResolverField("DefaultProviderTypeName").GetValue(null)!;
}
private static void SetDefaultProviderTypeName(string typeName)
{
GetResolverField("DefaultProviderTypeName").SetValue(null, typeName);
}
private static void ResetProvider()
{
GetResolverField("_provider").SetValue(null, null);
}
private static FieldInfo GetResolverField(string fieldName)
{
return typeof(LoggerFactoryResolver).GetField(
fieldName,
BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException(
$"Failed to locate LoggerFactoryResolver.{fieldName}.");
}
/// <summary>
/// 用于触发默认 provider 激活失败回退路径的测试桩。
/// </summary>
public sealed class ThrowingLoggerFactoryProvider : ILoggerFactoryProvider
{
/// <summary>
/// 初始化一个始终抛出异常的 provider。
/// </summary>
/// <exception cref="InvalidOperationException">始终抛出,用于覆盖回退路径。</exception>
public ThrowingLoggerFactoryProvider()
{
throw new InvalidOperationException("Simulated provider activation failure.");
}
/// <summary>
/// 获取或设置最小日志级别。
/// </summary>
public LogLevel MinLevel { get; set; } = LogLevel.Info;
/// <summary>
/// 创建日志器。
/// </summary>
/// <param name="name">日志器名称。</param>
/// <returns>该测试桩永远不会成功创建日志器。</returns>
/// <exception cref="NotSupportedException">始终抛出,因为该方法不应被调用。</exception>
public ILogger CreateLogger(string name)
{
throw new NotSupportedException();
}
}
/// <summary>
/// 用于验证并发首次初始化路径只创建单个 provider 实例的测试桩。
/// </summary>
public sealed class BlockingLoggerFactoryProvider : ILoggerFactoryProvider
{
private static int _constructionCount;
private static ManualResetEventSlim _constructionGate = new(false);
/// <summary>
/// 初始化一个会阻塞构造完成的 provider用于放大并发首次访问竞争窗口。
/// </summary>
public BlockingLoggerFactoryProvider()
{
Interlocked.Increment(ref _constructionCount);
_constructionGate.Wait(TimeSpan.FromSeconds(5));
}
/// <summary>
/// 获取已经发生的构造次数。
/// </summary>
public static int ConstructionCount => Volatile.Read(ref _constructionCount);
/// <summary>
/// 获取或设置最小日志级别。
/// </summary>
public LogLevel MinLevel { get; set; } = LogLevel.Info;
/// <summary>
/// 创建测试日志器。
/// </summary>
/// <param name="name">日志器名称。</param>
/// <returns>带有当前最小级别设置的控制台日志器。</returns>
public ILogger CreateLogger(string name)
{
return new ConsoleLogger(name, MinLevel, TextWriter.Null, false);
}
/// <summary>
/// 重置该测试桩的并发观测状态。
/// </summary>
public static void Reset()
{
_constructionGate = new ManualResetEventSlim(false);
Interlocked.Exchange(ref _constructionCount, 0);
}
/// <summary>
/// 释放当前被阻塞的 provider 构造过程。
/// </summary>
public static void ReleaseConstruction()
{
_constructionGate.Set();
}
}
}

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

@ -1,5 +1,6 @@
using System.Reflection;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.State;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Architectures;
@ -373,4 +374,4 @@ public class TestStateV5_2 : IState
}
}
#endregion
#endregion

View File

@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Environment;
@ -7,7 +9,6 @@ using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Environment;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Architectures;
@ -146,14 +147,51 @@ public abstract class Architecture : IArchitecture
#region Module Management
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 可以传入开放泛型行为类型,也可以传入绑定到特定请求的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
_modules.RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
_modules.RegisterMediatorBehavior<TBehavior>();
RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
_modules.RegisterCqrsHandlersFromAssembly(assembly);
}
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
_modules.RegisterCqrsHandlersFromAssemblies(assemblies);
}
/// <summary>
@ -328,4 +366,4 @@ public abstract class Architecture : IArchitecture
}
#endregion
}
}

View File

@ -1,7 +1,6 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Architectures;
@ -22,7 +21,7 @@ internal sealed class ArchitectureBootstrapper(
/// 因为用户初始化逻辑通常会立即访问事件总线、查询执行器或环境对象。
/// </summary>
/// <param name="existingContext">调用方已经提供的上下文;如果为空则创建默认上下文。</param>
/// <param name="configurator">可选的容器配置委托,用于接入 Mediator 等扩展服务。</param>
/// <param name="configurator">可选的容器配置委托,用于接入额外服务或覆盖默认依赖绑定。</param>
/// <param name="asyncMode">是否以异步模式初始化服务模块。</param>
/// <returns>已绑定到当前架构类型的架构上下文。</returns>
public async Task<IArchitectureContext> PrepareForInitializationAsync(
@ -92,16 +91,21 @@ internal sealed class ArchitectureBootstrapper(
/// <summary>
/// 为服务容器设置上下文并执行扩展配置钩子。
/// 这一步统一承接 Mediator 等容器扩展的接入点,避免 <see cref="Architecture" /> 直接操作容器细节。
/// 这一步统一承接 CQRS 运行时与容器扩展的接入点,避免 <see cref="Architecture" /> 直接操作容器细节。
/// </summary>
/// <param name="context">当前架构上下文。</param>
/// <param name="configurator">可选的服务集合配置委托。</param>
private void ConfigureServices(IArchitectureContext context, Action<IServiceCollection>? configurator)
{
services.SetContext(context);
services.Container.RegisterCqrsHandlersFromAssemblies(
[
architectureType.Assembly,
typeof(ArchitectureContext).Assembly
]);
if (configurator is null)
logger.Debug("Mediator-based cqrs will not take effect without the service setter configured!");
logger.Debug("No external service configurator provided. Using built-in CQRS runtime registration only.");
services.Container.ExecuteServicesHook(configurator);
}
@ -115,4 +119,4 @@ internal sealed class ArchitectureBootstrapper(
{
await services.ModuleManager.InitializeAllAsync(asyncMode);
}
}
}

View File

@ -8,7 +8,7 @@ using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Architectures;
@ -16,27 +16,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();
#region Mediator Integration
/// <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>
/// 获取 Mediator 实例(延迟加载)
/// 获取 CQRS runtime seam。
/// </summary>
private IMediator Mediator => GetOrCache<IMediator>();
/// <remarks>
/// 该实例会在首次访问时从容器解析,并通过 <see cref="Lazy{T}" /> 保证并发场景下只执行一次初始化,
/// 避免多个请求线程重复触发同一个 runtime 的容器解析。
/// </remarks>
private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value;
/// <summary>
/// 获取 ISender 实例(更轻量的发送器)
/// 从容器解析当前架构上下文依赖的 CQRS runtime。
/// </summary>
private ISender Sender => GetOrCache<ISender>();
/// <summary>
/// 获取 IPublisher 实例(用于发布通知)
/// </summary>
private IPublisher Publisher => GetOrCache<IPublisher>();
/// <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>
/// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存
@ -64,30 +85,23 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
}
/// <summary>
/// [Mediator] 发送请求Command/Query
/// 这是推荐的新方式,统一处理命令和查询
/// 发送请求Command/Query
/// 使用 GFramework 自有 CQRS runtime 统一处理命令和查询。
/// </summary>
/// <typeparam name="TResponse">响应类型</typeparam>
/// <param name="request">请求对象Command 或 Query</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>响应结果</returns>
/// <exception cref="InvalidOperationException">当 Mediator 未注册时抛出</exception>
public async ValueTask<TResponse> SendRequestAsync<TResponse>(
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var mediator = Mediator;
if (mediator == null)
throw new InvalidOperationException(
"Mediator not registered. Call EnableMediator() in your Architecture.OnInitialize() method.");
return await mediator.Send(request, cancellationToken);
return await CqrsRuntime.SendAsync(this, request, cancellationToken);
}
/// <summary>
/// [Mediator] 发送请求的同步版本(不推荐,仅用于兼容性)
/// 发送请求的同步版本(不推荐,仅用于兼容性)
/// </summary>
/// <typeparam name="TResponse">响应类型</typeparam>
/// <param name="request">请求对象</param>
@ -98,8 +112,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
}
/// <summary>
/// [Mediator] 发布通知(一对多)
/// 用于事件驱动场景,多个处理器可以同时处理同一个通知
/// 发布通知(一对多)
/// 使用 GFramework 自有 CQRS runtime 分发到所有已注册通知处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型</typeparam>
/// <param name="notification">通知对象</param>
@ -110,16 +124,11 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
where TNotification : INotification
{
ArgumentNullException.ThrowIfNull(notification);
var publisher = Publisher;
if (publisher == null)
throw new InvalidOperationException("Publisher not registered.");
await publisher.Publish(notification, cancellationToken);
await CqrsRuntime.PublishAsync(this, notification, cancellationToken);
}
/// <summary>
/// [Mediator] 发送请求并返回流(用于大数据集)
/// 发送请求并返回流(用于大数据集)
/// </summary>
/// <typeparam name="TResponse">响应项类型</typeparam>
/// <param name="request">流式请求</param>
@ -130,12 +139,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var mediator = Mediator;
if (mediator == null)
throw new InvalidOperationException("Mediator not registered.");
return mediator.CreateStream(request, cancellationToken);
return CqrsRuntime.CreateStream(this, request, cancellationToken);
}
/// <summary>
@ -171,7 +175,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>();
@ -180,12 +184,12 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
}
/// <summary>
/// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性)
/// 发送 CQRS 查询的同步版本(不推荐,仅用于兼容性)
/// </summary>
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <returns>查询结果</returns>
public TResponse SendQuery<TResponse>(Mediator.IQuery<TResponse> query)
public TResponse SendQuery<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
}
@ -205,23 +209,17 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
}
/// <summary>
/// [Mediator] 异步发送查询并返回结果
/// 通过Mediator模式发送查询请求支持取消操作
/// 异步发送 CQRS 查询并返回结果。
/// </summary>
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="query">要发送的查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含查询结果的ValueTask</returns>
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Mediator.IQuery<TResponse> query,
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
var sender = Sender;
if (sender == null)
throw new InvalidOperationException("Sender not registered.");
return await sender.Send(query, cancellationToken);
return await SendRequestAsync(query, cancellationToken);
}
#endregion
@ -347,23 +345,18 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
#region Command Execution
/// <summary>
/// [Mediator] 异步发送命令并返回结果
/// 通过Mediator模式发送命令请求支持取消操作
/// 异步发送 CQRS 命令并返回结果。
/// </summary>
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
/// <returns>包含命令执行结果的ValueTask</returns>
public async ValueTask<TResponse> SendCommandAsync<TResponse>(Mediator.ICommand<TResponse> command,
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(command);
var sender = Sender;
if (sender == null)
throw new InvalidOperationException("Sender not registered.");
return await sender.Send(command, cancellationToken);
return await SendRequestAsync(command, cancellationToken);
}
/// <summary>
@ -393,12 +386,12 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
}
/// <summary>
/// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性)
/// 发送 CQRS 命令的同步版本(不推荐,仅用于兼容性)
/// </summary>
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="command">要发送的命令对象</param>
/// <returns>命令执行结果</returns>
public TResponse SendCommand<TResponse>(Mediator.ICommand<TResponse> command)
public TResponse SendCommand<TResponse>(Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
}
@ -420,7 +413,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>();
@ -491,4 +484,4 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext
}
#endregion
}
}

View File

@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
@ -5,7 +7,7 @@ namespace GFramework.Core.Architectures;
/// <summary>
/// 架构模块管理器
/// 负责管理架构模块的安装和中介行为注册
/// 负责管理架构模块的安装和 CQRS 行为注册
/// </summary>
internal sealed class ArchitectureModules(
IArchitecture architecture,
@ -13,15 +15,56 @@ internal sealed class ArchitectureModules(
ILogger logger)
{
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 支持开放泛型行为类型和针对单一请求的封闭行为类型。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
logger.Debug($"Registering CQRS pipeline behavior: {typeof(TBehavior).Name}");
services.Container.RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}");
services.Container.RegisterMediatorBehavior<TBehavior>();
RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assembly);
logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}");
services.Container.RegisterCqrsHandlersFromAssembly(assembly);
}
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
logger.Debug("Registering CQRS handlers from additional assemblies.");
services.Container.RegisterCqrsHandlersFromAssemblies(assemblies);
}
/// <summary>
@ -37,4 +80,4 @@ internal sealed class ArchitectureModules(
logger.Info($"Module installed: {name}");
return module;
}
}
}

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;
@ -25,4 +25,4 @@ public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase,
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
protected abstract void OnExecute(TInput 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, 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 ICommand<TResult>.Execute()
TResult Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
@ -28,4 +28,4 @@ public abstract class AbstractCommand<TInput, TResult>(TInput input) : ContextAw
/// <param name="input">命令执行所需的输入参数</param>
/// <returns>命令执行后的结果</returns>
protected abstract TResult OnExecute(TInput 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

@ -0,0 +1,72 @@
using System.Runtime.ExceptionServices;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Rule;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Coroutine.Extensions;
/// <summary>
/// 提供 CQRS 命令与协程集成的扩展方法。
/// 这些扩展直接走架构上下文的内建 CQRS runtime不依赖外部 Mediator 服务。
/// </summary>
public static class CqrsCoroutineExtensions
{
/// <summary>
/// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。
/// </summary>
/// <typeparam name="TCommand">命令类型。</typeparam>
/// <param name="contextAware">上下文感知对象,用于获取架构上下文。</param>
/// <param name="command">要发送的命令对象。</param>
/// <param name="onError">发生异常时的回调处理函数。</param>
/// <returns>协程枚举器,用于协程执行。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="contextAware" /> 或 <paramref name="command" /> 为 <see langword="null" /> 时抛出。
/// </exception>
/// <exception cref="TaskCanceledException">
/// 当底层命令调度被取消且未提供 <paramref name="onError" /> 时抛出。
/// </exception>
/// <exception cref="Exception">
/// 当底层命令调度失败且未提供 <paramref name="onError" /> 时,抛出底层原始异常。
/// </exception>
/// <remarks>
/// 当底层命令调度失败时,该扩展会把底层异常解包后传给 <paramref name="onError" />
/// 在取消时则统一暴露 <see cref="TaskCanceledException" />,避免成功、失败与取消三种完成状态被混淆。
/// </remarks>
public static IEnumerator<IYieldInstruction> SendCommandCoroutine<TCommand>(
this IContextAware contextAware,
TCommand command,
Action<Exception>? onError = null)
where TCommand : IRequest<Unit>
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var task = contextAware.GetContext().SendAsync(command).AsTask();
yield return task.AsCoroutineInstruction();
if (task.IsCanceled)
{
// 取消态与成功态区分:协程层统一映射为 TaskCanceledException。
var canceledException = new TaskCanceledException(task);
if (onError != null)
{
onError.Invoke(canceledException);
yield break;
}
// 保留原始抛出栈,避免调试时丢失异常来源。
ExceptionDispatchInfo.Capture(canceledException).Throw();
}
if (!task.IsFaulted)
yield break;
// 优先解包业务异常,避免直接暴露 AggregateException。
var exception = task.Exception!.InnerException ?? task.Exception;
if (onError != null)
onError.Invoke(exception);
else
// 继续保留原始栈信息。
ExceptionDispatchInfo.Capture(exception).Throw();
}
}

View File

@ -11,22 +11,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Rule;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Coroutine.Extensions;
/// <summary>
/// 提供Mediator模式与协程集成的扩展方法。
/// 包含发送命令和等待事件的协程实现。
/// 提供 CQRS 命令与协程集成的扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="CqrsCoroutineExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Core.Coroutine.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class MediatorCoroutineExtensions
{
/// <summary>
/// 以协程方式发送命令并处理可能的异常。
/// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。
/// </summary>
/// <typeparam name="TCommand">命令的类型</typeparam>
/// <typeparam name="TCommand">命令的类型</typeparam>
/// <param name="contextAware">上下文感知对象,用于获取服务</param>
/// <param name="command">要发送的命令对象</param>
/// <param name="onError">发生异常时的回调处理函数</param>
@ -35,20 +40,8 @@ public static class MediatorCoroutineExtensions
this IContextAware contextAware,
TCommand command,
Action<Exception>? onError = null)
where TCommand : notnull
where TCommand : IRequest<Unit>
{
var mediator = contextAware
.GetContext()
.GetService<IMediator>()!;
var task = mediator.Send(command).AsTask();
yield return task.AsCoroutineInstruction();
if (!task.IsFaulted) yield break;
if (onError != null)
onError.Invoke(task.Exception!);
else
throw task.Exception!.InnerException ?? task.Exception;
return CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command, onError);
}
}
}

View File

@ -1,38 +0,0 @@
// 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.Rule;
using Mediator;
namespace GFramework.Core.Cqrs.Command;
/// <summary>
/// 抽象流式命令处理器基类
/// 继承自ContextAwareBase并实现IStreamCommandHandler接口为具体的流式命令处理器提供基础功能
/// 支持流式处理命令并产生异步可枚举的响应序列
/// </summary>
/// <typeparam name="TCommand">流式命令类型必须实现IStreamCommand接口</typeparam>
/// <typeparam name="TResponse">流式命令响应元素类型</typeparam>
public abstract class AbstractStreamCommandHandler<TCommand, TResponse> : ContextAwareBase,
IStreamCommandHandler<TCommand, TResponse>
where TCommand : IStreamCommand<TResponse>
{
/// <summary>
/// 处理流式命令并返回异步可枚举的响应序列
/// 由具体的流式命令处理器子类实现流式处理逻辑
/// </summary>
/// <param name="command">要处理的流式命令对象</param>
/// <param name="cancellationToken">取消令牌,用于取消流式处理操作</param>
/// <returns>异步可枚举的响应序列每个元素类型为TResponse</returns>
public abstract IAsyncEnumerable<TResponse> Handle(TCommand command, CancellationToken cancellationToken);
}

View File

@ -1,31 +0,0 @@
// 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.Cqrs.Notification;
using Mediator;
namespace GFramework.Core.Cqrs.Notification;
/// <summary>
/// 表示一个基础通知类,用于处理带有输入的通知模式实现。
/// 该类实现了 INotification 接口,提供了通用的通知结构。
/// </summary>
/// <typeparam name="TInput">通知输入数据的类型,必须实现 INotificationInput 接口</typeparam>
/// <param name="input">通知执行所需的输入数据</param>
public abstract class NotificationBase<TInput>(TInput input) : INotification where TInput : INotificationInput
{
/// <summary>
/// 获取通知的输入数据。
/// </summary>
public TInput Input => input;
}

View File

@ -1,38 +0,0 @@
// 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.Rule;
using Mediator;
namespace GFramework.Core.Cqrs.Query;
/// <summary>
/// 抽象流式查询处理器基类
/// 继承自ContextAwareBase并实现IStreamQueryHandler接口为具体的流式查询处理器提供基础功能
/// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景
/// </summary>
/// <typeparam name="TQuery">流式查询类型必须实现IStreamQuery接口</typeparam>
/// <typeparam name="TResponse">流式查询响应元素类型</typeparam>
public abstract class AbstractStreamQueryHandler<TQuery, TResponse> : ContextAwareBase,
IStreamQueryHandler<TQuery, TResponse>
where TQuery : IStreamQuery<TResponse>
{
/// <summary>
/// 处理流式查询并返回异步可枚举的响应序列
/// 由具体的流式查询处理器子类实现流式查询处理逻辑
/// </summary>
/// <param name="query">要处理的流式查询对象</param>
/// <param name="cancellationToken">取消令牌,用于取消流式查询操作</param>
/// <returns>异步可枚举的响应序列每个元素类型为TResponse</returns>
public abstract IAsyncEnumerable<TResponse> Handle(TQuery query, CancellationToken cancellationToken);
}

View File

@ -1,32 +0,0 @@
// 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.Cqrs.Query;
using Mediator;
namespace GFramework.Core.Cqrs.Query;
/// <summary>
/// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。
/// 该类继承自 Mediator.IQuery&lt;TResponse&gt; 接口,提供了通用的查询结构。
/// </summary>
/// <typeparam name="TInput">查询输入数据的类型,必须实现 IQueryInput 接口</typeparam>
/// <typeparam name="TResponse">查询执行后返回结果的类型</typeparam>
/// <param name="input">查询执行所需的输入数据</param>
public abstract class QueryBase<TInput, TResponse>(TInput input) : IQuery<TResponse> where TInput : IQueryInput
{
/// <summary>
/// 获取查询的输入数据。
/// </summary>
public TInput Input => input;
}

View File

@ -1,32 +0,0 @@
// 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.Cqrs.Request;
using Mediator;
namespace GFramework.Core.Cqrs.Request;
/// <summary>
/// 表示一个基础请求类,用于处理带有输入和响应的请求模式实现。
/// 该类实现了 IRequest&lt;TResponse&gt; 接口,提供了通用的请求结构。
/// </summary>
/// <typeparam name="TInput">请求输入数据的类型,必须实现 IRequestInput 接口</typeparam>
/// <typeparam name="TResponse">请求执行后返回结果的类型</typeparam>
/// <param name="input">请求执行所需的输入数据</param>
public abstract class RequestBase<TInput, TResponse>(TInput input) : IRequest<TResponse> where TInput : IRequestInput
{
/// <summary>
/// 获取请求的输入数据。
/// </summary>
public TInput Input => input;
}

View File

@ -1,16 +1,22 @@
using System.ComponentModel;
using GFramework.Core.Abstractions.Rule;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using GFramework.Cqrs.Extensions;
namespace GFramework.Core.Extensions;
/// <summary>
/// 提供对 IContextAware 接口的 Mediator 命令扩展方法
/// 使用 Mediator 库的命令模式
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="ContextAwareCqrsCommandExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class ContextAwareMediatorCommandExtensions
{
/// <summary>
/// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性)
/// 发送命令的同步版本(不推荐,仅用于兼容性)
/// </summary>
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="contextAware">实现 IContextAware 接口的对象</param>
@ -20,15 +26,11 @@ public static class ContextAwareMediatorCommandExtensions
public static TResponse SendCommand<TResponse>(this IContextAware contextAware,
ICommand<TResponse> command)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendCommand(command);
return ContextAwareCqrsCommandExtensions.SendCommand(contextAware, command);
}
/// <summary>
/// [Mediator] 异步发送命令并返回结果
/// 异步发送命令并返回结果
/// </summary>
/// <typeparam name="TResponse">命令响应类型</typeparam>
/// <param name="contextAware">实现 IContextAware 接口的对象</param>
@ -39,10 +41,9 @@ public static class ContextAwareMediatorCommandExtensions
public static ValueTask<TResponse> SendCommandAsync<TResponse>(this IContextAware contextAware,
ICommand<TResponse> command, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendCommandAsync(command, cancellationToken);
return ContextAwareCqrsCommandExtensions.SendCommandAsync(
contextAware,
command,
cancellationToken);
}
}
}

View File

@ -1,11 +1,18 @@
using System.ComponentModel;
using GFramework.Core.Abstractions.Rule;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Extensions;
namespace GFramework.Core.Extensions;
/// <summary>
/// 提供对 IContextAware 接口的 Mediator 统一接口扩展方法
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一接口扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="ContextAwareCqrsExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Core.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class ContextAwareMediatorExtensions
{
/// <summary>
@ -20,11 +27,10 @@ public static class ContextAwareMediatorExtensions
public static ValueTask<TResponse> SendRequestAsync<TResponse>(this IContextAware contextAware,
IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
var context = contextAware.GetContext();
return context.SendRequestAsync(request, cancellationToken);
return ContextAwareCqrsExtensions.SendRequestAsync(
contextAware,
request,
cancellationToken);
}
/// <summary>
@ -38,11 +44,7 @@ public static class ContextAwareMediatorExtensions
public static TResponse SendRequest<TResponse>(this IContextAware contextAware,
IRequest<TResponse> request)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
var context = contextAware.GetContext();
return context.SendRequest(request);
return ContextAwareCqrsExtensions.SendRequest(contextAware, request);
}
/// <summary>
@ -58,11 +60,10 @@ public static class ContextAwareMediatorExtensions
TNotification notification, CancellationToken cancellationToken = default)
where TNotification : INotification
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(notification);
var context = contextAware.GetContext();
return context.PublishAsync(notification, cancellationToken);
return ContextAwareCqrsExtensions.PublishAsync(
contextAware,
notification,
cancellationToken);
}
/// <summary>
@ -77,11 +78,10 @@ public static class ContextAwareMediatorExtensions
public static IAsyncEnumerable<TResponse> CreateStream<TResponse>(this IContextAware contextAware,
IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(request);
var context = contextAware.GetContext();
return context.CreateStream(request, cancellationToken);
return ContextAwareCqrsExtensions.CreateStream(
contextAware,
request,
cancellationToken);
}
/// <summary>
@ -97,11 +97,10 @@ public static class ContextAwareMediatorExtensions
CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendAsync(command, cancellationToken);
return ContextAwareCqrsExtensions.SendAsync(
contextAware,
command,
cancellationToken);
}
/// <summary>
@ -116,10 +115,9 @@ public static class ContextAwareMediatorExtensions
public static ValueTask<TResponse> SendAsync<TResponse>(this IContextAware contextAware,
IRequest<TResponse> command, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendAsync(command, cancellationToken);
return ContextAwareCqrsExtensions.SendAsync(
contextAware,
command,
cancellationToken);
}
}
}

View File

@ -1,16 +1,22 @@
using System.ComponentModel;
using GFramework.Core.Abstractions.Rule;
using Mediator;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
using GFramework.Cqrs.Extensions;
namespace GFramework.Core.Extensions;
/// <summary>
/// 提供对 IContextAware 接口的 Mediator 查询扩展方法
/// 使用 Mediator 库的查询模式
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="ContextAwareCqrsQueryExtensions" />。
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use GFramework.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")]
public static class ContextAwareMediatorQueryExtensions
{
/// <summary>
/// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性)
/// 发送查询的同步版本(不推荐,仅用于兼容性)
/// </summary>
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="contextAware">实现 IContextAware 接口的对象</param>
@ -19,15 +25,11 @@ public static class ContextAwareMediatorQueryExtensions
/// <exception cref="ArgumentNullException">当 contextAware 或 query 为 null 时抛出</exception>
public static TResponse SendQuery<TResponse>(this IContextAware contextAware, IQuery<TResponse> query)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
var context = contextAware.GetContext();
return context.SendQuery(query);
return ContextAwareCqrsQueryExtensions.SendQuery(contextAware, query);
}
/// <summary>
/// [Mediator] 异步发送查询并返回结果
/// 异步发送查询并返回结果
/// </summary>
/// <typeparam name="TResponse">查询响应类型</typeparam>
/// <param name="contextAware">实现 IContextAware 接口的对象</param>
@ -38,10 +40,9 @@ public static class ContextAwareMediatorQueryExtensions
public static ValueTask<TResponse> SendQueryAsync<TResponse>(this IContextAware contextAware,
IQuery<TResponse> query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
var context = contextAware.GetContext();
return context.SendQueryAsync(query, cancellationToken);
return ContextAwareCqrsQueryExtensions.SendQueryAsync(
contextAware,
query,
cancellationToken);
}
}
}

View File

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

View File

@ -16,4 +16,5 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Channels;
global using System.Threading.Channels;
global using Microsoft.Extensions.DependencyInjection;

View File

@ -1,11 +1,12 @@
using System.ComponentModel;
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Logging;
using GFramework.Core.Rule;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Ioc;
@ -34,6 +35,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
#endregion
/// <summary>
/// 记录某个实例在未冻结查询中可见的服务类型分组信息。
/// </summary>
/// <param name="ServiceType">当前分组对应的服务类型。</param>
/// <param name="Count">该服务类型下的描述符数量。</param>
/// <param name="FirstIndex">该服务类型首次出现的位置,用于稳定打破并列。</param>
private sealed record VisibleServiceTypeGroup(Type ServiceType, int Count, int FirstIndex);
#region Fields
/// <summary>
@ -310,13 +319,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <summary>
/// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 注册 CQRS 请求管道行为。
/// 同时支持开放泛型行为类型和已闭合的具体行为类型,
/// 以兼容通用行为和针对单一请求的专用行为两种注册方式。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
_lock.EnterWriteLock();
try
@ -351,7 +359,62 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
_logger.Debug($"Mediator behavior registered: {behaviorType.Name}");
_logger.Debug($"CQRS pipeline behavior registered: {behaviorType.Name}");
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 注册 CQRS 请求管道行为。
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
/// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete(
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
{
RegisterCqrsPipelineBehavior<TBehavior>();
}
/// <summary>
/// 从指定程序集显式注册 CQRS 处理器。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assembly);
RegisterCqrsHandlersFromAssemblies([assembly]);
}
/// <summary>
/// 从多个程序集显式注册 CQRS 处理器。
/// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 中存在 <see langword="null" /> 元素。</exception>
/// <exception cref="InvalidOperationException"> 容器已冻结,无法继续注册 CQRS 处理器。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
ArgumentNullException.ThrowIfNull(assemblies);
var assemblyArray = assemblies.ToArray();
foreach (var assembly in assemblyArray)
{
ArgumentNullException.ThrowIfNull(assembly);
}
_lock.EnterWriteLock();
try
{
ThrowIfFrozen();
ResolveCqrsRegistrationService().RegisterHandlers(assemblyArray);
}
finally
{
@ -381,6 +444,27 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
#region Get
/// <summary>
/// 获取当前容器中已注册的 CQRS 程序集注册协调器。
/// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定,
/// 避免在容器未冻结前依赖完整的服务提供者构建流程。
/// </summary>
/// <returns>已注册的 CQRS 程序集注册协调器实例。</returns>
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 程序集注册协调器实例时抛出。</exception>
private ICqrsRegistrationService ResolveCqrsRegistrationService()
{
var descriptor = GetServicesUnsafe.LastOrDefault(static service =>
service.ServiceType == typeof(ICqrsRegistrationService));
if (descriptor?.ImplementationInstance is ICqrsRegistrationService registrationService)
return registrationService;
const string errorMessage =
"ICqrsRegistrationService not registered. Ensure the CQRS runtime module has been installed before registering handlers.";
_logger.Error(errorMessage);
throw new InvalidOperationException(errorMessage);
}
/// <summary>
/// 获取指定泛型类型的服务实例
/// 返回第一个匹配的注册实例如果不存在则返回null
@ -523,29 +607,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
{
if (_provider == null)
{
// 如果容器未冻结,从服务集合中获取已注册的实例
var serviceType = typeof(T);
var registeredServices = GetServicesUnsafe
.Where(s => s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType)).ToList();
var result = new List<T>();
foreach (var descriptor in registeredServices)
{
if (descriptor.ImplementationInstance is T instance)
{
result.Add(instance);
}
else if (descriptor.ImplementationFactory != null)
{
// 在未冻结状态下无法调用工厂方法,跳过
}
else if (descriptor.ImplementationType != null)
{
// 在未冻结状态下无法创建实例,跳过
}
}
return result;
return CollectRegisteredImplementationInstances(typeof(T)).Cast<T>().ToList();
}
var services = _provider!.GetServices<T>().ToList();
@ -563,37 +625,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
/// <param name="type">服务类型</param>
/// <returns>只读的服务实例列表</returns>
/// <exception cref="InvalidOperationException">当容器未冻结时抛出</exception>
/// <exception cref="ArgumentNullException">当 <paramref name="type" /> 为 <see langword="null" /> 时抛出</exception>
public IReadOnlyList<object> GetAll(Type type)
{
ArgumentNullException.ThrowIfNull(type);
_lock.EnterReadLock();
try
{
if (_provider == null)
{
// 如果容器未冻结,从服务集合中获取已注册的实例
var registeredServices = GetServicesUnsafe
.Where(s => s.ServiceType == type || type.IsAssignableFrom(s.ServiceType))
.ToList();
var result = new List<object>();
foreach (var descriptor in registeredServices)
{
if (descriptor.ImplementationInstance != null)
{
result.Add(descriptor.ImplementationInstance);
}
else if (descriptor.ImplementationFactory != null)
{
// 在未冻结状态下无法调用工厂方法,跳过
}
else if (descriptor.ImplementationType != null)
{
// 在未冻结状态下无法创建实例,跳过
}
}
return result;
return CollectRegisteredImplementationInstances(type);
}
var services = _provider!.GetServices(type).ToList();
@ -606,6 +648,108 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 在容器未冻结时,从服务描述符中收集当前可直接观察到的实例绑定。
/// </summary>
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
/// <returns>按当前未冻结语义可见的实例列表。</returns>
/// <remarks>
/// 该方法只读取 <see cref="ServiceDescriptor.ImplementationInstance" />,因为未冻结路径不会主动执行工厂方法,
/// 也不会提前构造 <see cref="ServiceDescriptor.ImplementationType" />。
/// 若同一实例同时经由多个可赋值的 <see cref="ServiceDescriptor.ServiceType" /> 暴露,
/// 这里会把它视为兼容别名并只保留一个规范服务类型对应的结果;
/// 但同一 <see cref="ServiceDescriptor.ServiceType" /> 的重复显式注册仍会完整保留,以维持注册顺序和多次注册语义。
/// </remarks>
private List<object> CollectRegisteredImplementationInstances(Type requestedServiceType)
{
ArgumentNullException.ThrowIfNull(requestedServiceType);
var matchingDescriptors = GetServicesUnsafe
.Where(descriptor =>
descriptor.ServiceType == requestedServiceType ||
requestedServiceType.IsAssignableFrom(descriptor.ServiceType))
.ToList();
if (matchingDescriptors.Count == 0)
return [];
var preferredServiceTypes = BuildPreferredVisibleServiceTypes(matchingDescriptors, requestedServiceType);
var result = new List<object>();
foreach (var descriptor in matchingDescriptors)
{
if (descriptor.ImplementationInstance is { } instance)
{
if (preferredServiceTypes.TryGetValue(instance, out var preferredServiceType) &&
preferredServiceType == descriptor.ServiceType)
{
result.Add(instance);
}
}
else if (descriptor.ImplementationFactory != null)
{
// 在未冻结状态下无法调用工厂方法,跳过。
}
else if (descriptor.ImplementationType != null)
{
// 在未冻结状态下无法创建实例,跳过。
}
}
return result;
}
/// <summary>
/// 为每个可见实例选择一个规范服务类型,避免同一实例因兼容别名重复出现在未冻结查询结果中。
/// </summary>
/// <param name="matchingDescriptors">已按请求类型过滤过的服务描述符集合。</param>
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
/// <returns>实例到其规范服务类型的映射。</returns>
private static Dictionary<object, Type> BuildPreferredVisibleServiceTypes(
IReadOnlyList<ServiceDescriptor> matchingDescriptors,
Type requestedServiceType)
{
var preferredServiceTypes = new Dictionary<object, Type>(ReferenceEqualityComparer.Instance);
foreach (var instanceGroup in matchingDescriptors
.Where(static descriptor => descriptor.ImplementationInstance is not null)
.GroupBy(static descriptor => descriptor.ImplementationInstance!,
ReferenceEqualityComparer.Instance))
{
preferredServiceTypes.Add(
instanceGroup.Key,
SelectPreferredVisibleServiceType(instanceGroup, requestedServiceType));
}
return preferredServiceTypes;
}
/// <summary>
/// 在“同一实例被多个服务类型暴露”的场景下,选择未冻结查询结果应保留的规范服务类型。
/// </summary>
/// <param name="descriptorsForInstance">引用同一实例的服务描述符。</param>
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
/// <returns>应在结果中保留的服务类型。</returns>
private static Type SelectPreferredVisibleServiceType(
IEnumerable<ServiceDescriptor> descriptorsForInstance,
Type requestedServiceType)
{
var serviceTypeGroups = descriptorsForInstance
.GroupBy(static descriptor => descriptor.ServiceType)
.Select((group, index) => new VisibleServiceTypeGroup(group.Key, group.Count(), index))
.ToList();
// 若调用方请求的正是其中一个服务类型,优先保留它,使未冻结行为尽量贴近冻结后的精确服务解析口径。
var requestedGroup = serviceTypeGroups.FirstOrDefault(group => group.ServiceType == requestedServiceType);
if (requestedGroup is not null)
return requestedGroup.ServiceType;
// 否则优先保留“同一服务类型下注册次数最多”的那组,避免显式多次注册被较宽泛的别名折叠掉。
return serviceTypeGroups
.OrderByDescending(static group => group.Count)
.ThenBy(static group => group.FirstIndex)
.First()
.ServiceType;
}
/// <summary>
/// 获取并排序指定泛型类型的所有服务实例
/// 主要用于系统调度场景
@ -804,4 +948,4 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
#endregion
}
}

View File

@ -1,26 +0,0 @@
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Logging;
/// <summary>
/// 日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例
/// </summary>
public static class LoggerFactoryResolver
{
/// <summary>
/// 获取或设置当前的日志工厂提供程序
/// </summary>
/// <value>
/// 日志工厂提供程序实例,默认为控制台日志工厂提供程序
/// </value>
public static ILoggerFactoryProvider Provider { get; set; }
= new ConsoleLoggerFactoryProvider();
/// <summary>
/// 获取或设置日志记录的最小级别
/// </summary>
/// <value>
/// 日志级别枚举值默认为Info级别
/// </value>
public static LogLevel MinLevel { get; set; } = LogLevel.Info;
}

View File

@ -0,0 +1,12 @@
using System.Runtime.CompilerServices;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Command;
using GFramework.Cqrs.Notification;
using GFramework.Cqrs.Query;
using GFramework.Cqrs.Request;
[assembly: TypeForwardedTo(typeof(LoggerFactoryResolver))]
[assembly: TypeForwardedTo(typeof(CommandBase<,>))]
[assembly: TypeForwardedTo(typeof(QueryBase<,>))]
[assembly: TypeForwardedTo(typeof(RequestBase<,>))]
[assembly: TypeForwardedTo(typeof(NotificationBase<>))]

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, IQuery<TResult>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
@ -27,4 +27,4 @@ public abstract class AbstractQuery<TInput, TResult>(TInput input) : ContextAwar
/// <param name="input">查询输入参数</param>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo(TInput input);
}
}

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,68 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
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("CqrsDispatcher");
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger);
var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
container.Register(runtime);
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime);
container.Register<ICqrsHandlerRegistrar>(registrar);
container.Register<ICqrsRegistrationService>(
CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger));
}
/// <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,13 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义 CQRS runtime 在分发期间携带的最小上下文标记。
/// </summary>
/// <remarks>
/// 该接口当前刻意保持为轻量 marker seam只用于让 <see cref="ICqrsRuntime" /> 从
/// <c>GFramework.Core.Abstractions</c> 的 <c>IArchitectureContext</c> 解耦。
/// 运行时实现仍可在需要时识别更具体的上下文类型,并对现有 <c>IContextAware</c> 处理器执行兼容注入。
/// </remarks>
public interface ICqrsContext
{
}

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

@ -0,0 +1,81 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义架构上下文使用的 CQRS runtime seam。
/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦,
/// 使 CQRS runtime 契约可独立归属到 <c>GFramework.Cqrs.Abstractions</c>。
/// </summary>
public interface ICqrsRuntime
{
/// <summary>
/// 发送请求并返回响应。
/// </summary>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="request">要分发的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="context" /> 或 <paramref name="request" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// 当前上下文无法满足运行时要求,例如未找到对应请求处理器,或请求处理链中的
/// <c>IContextAware</c> 对象需要 <c>IArchitectureContext</c> 但当前 <paramref name="context" /> 不提供该能力。
/// </exception>
/// <remarks>
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
/// </remarks>
ValueTask<TResponse> SendAsync<TResponse>(
ICqrsContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default);
/// <summary>
/// 发布通知到所有已注册处理器。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>表示通知分发完成的值任务。</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="context" /> 或 <paramref name="notification" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// 已解析到的通知处理器需要框架级上下文注入,但当前 <paramref name="context" /> 不提供
/// <c>IArchitectureContext</c> 能力。
/// </exception>
/// <remarks>
/// 默认实现允许零处理器场景静默完成;只有在处理器注入前置条件不满足时才会抛出异常。
/// </remarks>
ValueTask PublishAsync<TNotification>(
ICqrsContext context,
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification;
/// <summary>
/// 创建流式请求的异步响应序列。
/// </summary>
/// <typeparam name="TResponse">流元素类型。</typeparam>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>按需生成的异步响应序列。</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="context" /> 或 <paramref name="request" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// 当前上下文无法满足运行时要求,例如未找到对应流式处理器,或流式处理链中的
/// <c>IContextAware</c> 对象需要 <c>IArchitectureContext</c> 但当前 <paramref name="context" /> 不提供该能力。
/// </exception>
/// <remarks>
/// 返回的异步序列在枚举前通常已完成处理器解析与上下文注入,
/// 因此调用方应把 <paramref name="context" /> 视为整个枚举生命周期内的必需依赖。
/// </remarks>
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
ICqrsContext context,
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default);
}

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

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

View File

@ -0,0 +1,17 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示处理通知消息的处理器契约。
/// </summary>
/// <typeparam name="TNotification">通知类型。</typeparam>
public interface INotificationHandler<in TNotification>
where TNotification : INotification
{
/// <summary>
/// 处理通知消息。
/// </summary>
/// <param name="notification">要处理的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步处理任务。</returns>
ValueTask Handle(TNotification notification, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,22 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 定义 CQRS 请求处理前后的管道行为。
/// </summary>
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
public interface IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <summary>
/// 处理当前请求,并决定是否继续调用后续行为或最终处理器。
/// </summary>
/// <param name="message">当前请求消息。</param>
/// <param name="next">下一个处理委托。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
ValueTask<TResponse> Handle(
TRequest message,
MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken);
}

View File

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

View File

@ -0,0 +1,18 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示处理单个 CQRS 请求的处理器契约。
/// </summary>
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
public interface IRequestHandler<in TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <summary>
/// 处理指定请求并返回结果。
/// </summary>
/// <param name="request">要处理的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求结果。</returns>
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}

View File

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

View File

@ -0,0 +1,18 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示处理流式 CQRS 请求的处理器契约。
/// </summary>
/// <typeparam name="TRequest">流式请求类型。</typeparam>
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
public interface IStreamRequestHandler<in TRequest, out TResponse>
where TRequest : IStreamRequest<TResponse>
{
/// <summary>
/// 处理流式请求并返回异步响应序列。
/// </summary>
/// <param name="request">要处理的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应序列。</returns>
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,19 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示 CQRS 请求在管道中继续向下执行的处理委托。
/// </summary>
/// <remarks>
/// <para>管道行为可以通过不调用该委托来短路请求处理。</para>
/// <para>除显式实现重试等高级语义外,行为通常应最多调用一次该委托,以维持单次请求分发的确定性。</para>
/// <para>调用方应传递当前收到的 <paramref name="cancellationToken" />,确保取消信号沿整条管道一致传播。</para>
/// </remarks>
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="message">当前请求消息。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
public delegate ValueTask<TResponse> MessageHandlerDelegate<in TRequest, TResponse>(
TRequest message,
CancellationToken cancellationToken)
where TRequest : IRequest<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.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

@ -0,0 +1,13 @@
namespace GFramework.Cqrs.Abstractions.Cqrs;
/// <summary>
/// 表示没有实际返回值的 CQRS 响应类型。
/// 该类型用于统一命令与请求的泛型签名,避免引入外部库的 <c>Unit</c> 定义。
/// </summary>
public readonly record struct Unit
{
/// <summary>
/// 获取默认的空响应实例。
/// </summary>
public static Unit Value { get; } = new();
}

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

@ -0,0 +1,174 @@
// 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.Architectures;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine.Extensions;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Coroutine;
/// <summary>
/// <see cref="CqrsCoroutineExtensions" /> 的单元测试类。
/// 验证新的 CQRS 协程扩展直接走框架内建 CQRS runtime
/// 并确保协程对命令调度异常的传播行为保持稳定。
/// </summary>
[TestFixture]
public class CqrsCoroutineExtensionsTests
{
/// <summary>
/// 验证SendCommandCoroutine应该返回IEnumerator<IYieldInstruction>
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Return_IEnumerator_Of_YieldInstruction()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(ValueTask.CompletedTask);
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine, Is.InstanceOf<IEnumerator<IYieldInstruction>>());
}
/// <summary>
/// 验证 SendCommandCoroutine 在底层命令调度失败时会重新抛出原始异常。
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Rethrow_Inner_Exception_When_Command_Fails()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
var expectedException = new InvalidOperationException("Command failed.");
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(new ValueTask(Task.FromException(expectedException)));
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine.MoveNext(), Is.True);
var exception = Assert.Throws<InvalidOperationException>(() => coroutine.MoveNext());
Assert.That(exception, Is.SameAs(expectedException));
}
/// <summary>
/// 验证 SendCommandCoroutine 在提供错误回调时也会传递解包后的原始异常,
/// 避免回调路径暴露 <see cref="AggregateException" />。
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Forward_Inner_Exception_To_Error_Handler()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
var expectedException = new InvalidOperationException("Command failed.");
Exception? capturedException = null;
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(new ValueTask(Task.FromException(expectedException)));
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(
contextAware,
command,
exception => capturedException = exception);
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.MoveNext(), Is.False);
Assert.That(capturedException, Is.SameAs(expectedException));
}
/// <summary>
/// 验证 SendCommandCoroutine 在底层命令被取消且未提供错误回调时会抛出取消异常。
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Throw_TaskCanceledException_When_Command_Is_Canceled()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token)));
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command);
Assert.That(coroutine.MoveNext(), Is.True);
Assert.Throws<TaskCanceledException>(() => coroutine.MoveNext());
}
/// <summary>
/// 验证 SendCommandCoroutine 在底层命令被取消且提供错误回调时会把取消异常转发给回调。
/// </summary>
[Test]
public void SendCommandCoroutine_Should_Forward_TaskCanceledException_To_Error_Handler_When_Command_Is_Canceled()
{
var command = new TestCommand("Test");
var contextAware = new TestContextAware();
using var cancellationTokenSource = new CancellationTokenSource();
Exception? capturedException = null;
cancellationTokenSource.Cancel();
contextAware.MockContext
.Setup(ctx => ctx.SendAsync(command, It.IsAny<CancellationToken>()))
.Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token)));
var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(
contextAware,
command,
exception => capturedException = exception);
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.MoveNext(), Is.False);
Assert.That(capturedException, Is.TypeOf<TaskCanceledException>());
}
/// <summary>
/// 测试用的简单命令类
/// </summary>
private sealed record TestCommand(string Data) : IRequest<Unit>;
/// <summary>
/// 上下文感知基类的模拟实现
/// </summary>
private sealed class TestContextAware : IContextAware
{
/// <summary>
/// 提供可配置的架构上下文 Mock。
/// </summary>
public Mock<IArchitectureContext> MockContext { get; } = new();
/// <summary>
/// 获取当前架构上下文。
/// </summary>
/// <returns>用于 CQRS 调用的架构上下文实例。</returns>
public IArchitectureContext GetContext()
{
return MockContext.Object;
}
/// <summary>
/// 设置架构上下文。
/// </summary>
/// <param name="context">要设置的架构上下文。</param>
public void SetContext(IArchitectureContext context)
{
}
}
}

View File

@ -0,0 +1,74 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Rule;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using GFramework.Cqrs.Cqrs.Command;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 CQRS handler 基类在脱离 dispatcher 使用时会显式失败,并在注入上下文后保持可观察行为。
/// </summary>
[TestFixture]
internal sealed class AbstractCqrsHandlerContextTests
{
/// <summary>
/// 验证新的轻量 handler 基类不会再偷偷回退到全局 GameContext。
/// </summary>
[Test]
public void GetContext_Should_Throw_When_Handler_Has_Not_Been_Initialized_By_Runtime()
{
var handler = new TestCommandHandler();
var exception = Assert.Throws<InvalidOperationException>(() => ((IContextAware)handler).GetContext());
Assert.That(
exception!.Message,
Does.Contain("has not been initialized").IgnoreCase);
}
/// <summary>
/// 验证 runtime 注入上下文后,派生 handler 可以继续访问 Context 并收到 OnContextReady 回调。
/// </summary>
[Test]
public async Task Handle_Should_Observe_Injected_Context_And_OnContextReady_Callback()
{
var handler = new TestCommandHandler();
var context = new Mock<IArchitectureContext>(MockBehavior.Strict).Object;
((IContextAware)handler).SetContext(context);
await handler.Handle(new TestCommand(), CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(handler.OnContextReadyCallCount, Is.EqualTo(1));
Assert.That(handler.LastObservedContext, Is.SameAs(context));
});
}
/// <summary>
/// 用于验证上下文注入行为的最小 CQRS 命令。
/// </summary>
private sealed record TestCommand : ICommand<Unit>;
/// <summary>
/// 暴露基类上下文访问与初始化回调的测试处理器。
/// </summary>
private sealed class TestCommandHandler : AbstractCommandHandler<TestCommand>
{
public int OnContextReadyCallCount { get; private set; }
public IArchitectureContext? LastObservedContext { get; private set; }
protected override void OnContextReady()
{
OnContextReadyCallCount++;
}
public override ValueTask<Unit> Handle(TestCommand command, CancellationToken cancellationToken)
{
LastObservedContext = Context;
return ValueTask.FromResult(Unit.Value);
}
}
}

View File

@ -0,0 +1,331 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 CQRS dispatcher 会缓存热路径中的服务类型与调用委托。
/// </summary>
[TestFixture]
internal sealed class CqrsDispatcherCacheTests
{
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 初始化测试上下文。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
CqrsTestRuntime.RegisterHandlers(
_container,
typeof(CqrsDispatcherCacheTests).Assembly,
typeof(ArchitectureContext).Assembly);
_container.Freeze();
_context = new ArchitectureContext(_container);
ClearDispatcherCaches();
}
/// <summary>
/// 清理测试上下文引用。
/// </summary>
[TearDown]
public void TearDown()
{
_context = null;
_container = null;
}
/// <summary>
/// 验证相同消息类型重复分发时,不会重复扩张服务类型与调用委托缓存。
/// </summary>
[Test]
public async Task Dispatcher_Should_Cache_Service_Types_After_First_Dispatch()
{
var notificationServiceTypes = GetCacheField("NotificationHandlerServiceTypes");
var requestServiceTypes = GetCacheField("RequestServiceTypes");
var streamServiceTypes = GetCacheField("StreamHandlerServiceTypes");
var requestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers");
var requestPipelineInvokers = GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers");
var notificationInvokers = GetCacheField("NotificationInvokers");
var streamInvokers = GetCacheField("StreamInvokers");
var notificationBefore = notificationServiceTypes.Count;
var requestBefore = requestServiceTypes.Count;
var streamBefore = streamServiceTypes.Count;
var requestInvokersBefore = requestInvokers.Count;
var requestPipelineInvokersBefore = requestPipelineInvokers.Count;
var notificationInvokersBefore = notificationInvokers.Count;
var streamInvokersBefore = streamInvokers.Count;
await _context!.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
await _context.PublishAsync(new DispatcherCacheNotification());
await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest()));
var notificationAfterFirstDispatch = notificationServiceTypes.Count;
var requestAfterFirstDispatch = requestServiceTypes.Count;
var streamAfterFirstDispatch = streamServiceTypes.Count;
var requestInvokersAfterFirstDispatch = requestInvokers.Count;
var requestPipelineInvokersAfterFirstDispatch = requestPipelineInvokers.Count;
var notificationInvokersAfterFirstDispatch = notificationInvokers.Count;
var streamInvokersAfterFirstDispatch = streamInvokers.Count;
await _context.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
await _context.PublishAsync(new DispatcherCacheNotification());
await DrainAsync(_context.CreateStream(new DispatcherCacheStreamRequest()));
Assert.Multiple(() =>
{
Assert.That(notificationAfterFirstDispatch, Is.EqualTo(notificationBefore + 1));
Assert.That(requestAfterFirstDispatch, Is.EqualTo(requestBefore + 2));
Assert.That(streamAfterFirstDispatch, Is.EqualTo(streamBefore + 1));
Assert.That(requestInvokersAfterFirstDispatch, Is.EqualTo(requestInvokersBefore + 1));
Assert.That(requestPipelineInvokersAfterFirstDispatch, Is.EqualTo(requestPipelineInvokersBefore + 1));
Assert.That(notificationInvokersAfterFirstDispatch, Is.EqualTo(notificationInvokersBefore + 1));
Assert.That(streamInvokersAfterFirstDispatch, Is.EqualTo(streamInvokersBefore + 1));
Assert.That(notificationServiceTypes.Count, Is.EqualTo(notificationAfterFirstDispatch));
Assert.That(requestServiceTypes.Count, Is.EqualTo(requestAfterFirstDispatch));
Assert.That(streamServiceTypes.Count, Is.EqualTo(streamAfterFirstDispatch));
Assert.That(requestInvokers.Count, Is.EqualTo(requestInvokersAfterFirstDispatch));
Assert.That(requestPipelineInvokers.Count, Is.EqualTo(requestPipelineInvokersAfterFirstDispatch));
Assert.That(notificationInvokers.Count, Is.EqualTo(notificationInvokersAfterFirstDispatch));
Assert.That(streamInvokers.Count, Is.EqualTo(streamInvokersAfterFirstDispatch));
});
}
/// <summary>
/// 验证 request 调用委托会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。
/// </summary>
[Test]
public async Task Dispatcher_Should_Cache_Request_Invokers_Per_Response_Type()
{
var intRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers");
var stringRequestInvokers = GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers");
var intBefore = intRequestInvokers.Count;
var stringBefore = stringRequestInvokers.Count;
await _context!.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherStringCacheRequest());
var intAfterFirstDispatch = intRequestInvokers.Count;
var stringAfterFirstDispatch = stringRequestInvokers.Count;
await _context.SendRequestAsync(new DispatcherCacheRequest());
await _context.SendRequestAsync(new DispatcherStringCacheRequest());
Assert.Multiple(() =>
{
Assert.That(intAfterFirstDispatch, Is.EqualTo(intBefore + 1));
Assert.That(stringAfterFirstDispatch, Is.EqualTo(stringBefore + 1));
Assert.That(intRequestInvokers.Count, Is.EqualTo(intAfterFirstDispatch));
Assert.That(stringRequestInvokers.Count, Is.EqualTo(stringAfterFirstDispatch));
});
}
/// <summary>
/// 通过反射读取 dispatcher 的静态缓存字典。
/// </summary>
private static IDictionary GetCacheField(string fieldName)
{
var dispatcherType = GetDispatcherType();
var field = dispatcherType.GetField(
fieldName,
BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}.");
return field!.GetValue(null) as IDictionary
?? throw new InvalidOperationException(
$"Dispatcher cache field {fieldName} does not implement IDictionary.");
}
/// <summary>
/// 清空本测试依赖的 dispatcher 静态缓存,避免跨用例共享进程级状态导致断言漂移。
/// </summary>
private static void ClearDispatcherCaches()
{
GetCacheField("NotificationHandlerServiceTypes").Clear();
GetCacheField("RequestServiceTypes").Clear();
GetCacheField("StreamHandlerServiceTypes").Clear();
GetCacheField("NotificationInvokers").Clear();
GetCacheField("StreamInvokers").Clear();
GetGenericCacheField("RequestInvokerCache`1", typeof(int), "Invokers").Clear();
GetGenericCacheField("RequestInvokerCache`1", typeof(string), "Invokers").Clear();
GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(int), "Invokers").Clear();
GetGenericCacheField("RequestPipelineInvokerCache`1", typeof(string), "Invokers").Clear();
}
/// <summary>
/// 通过反射读取 dispatcher 嵌套泛型缓存类型上的静态缓存字典。
/// </summary>
private static IDictionary GetGenericCacheField(string nestedTypeName, Type genericTypeArgument, string fieldName)
{
var nestedGenericType = GetDispatcherType().GetNestedType(
nestedTypeName,
BindingFlags.NonPublic);
Assert.That(nestedGenericType, Is.Not.Null, $"Missing dispatcher nested cache type {nestedTypeName}.");
var closedNestedType = nestedGenericType!.MakeGenericType(genericTypeArgument);
var field = closedNestedType.GetField(
fieldName,
BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(
field,
Is.Not.Null,
$"Missing dispatcher nested cache field {nestedTypeName}.{fieldName} for {genericTypeArgument.FullName}.");
return field!.GetValue(null) as IDictionary
?? throw new InvalidOperationException(
$"Dispatcher nested cache field {nestedTypeName}.{fieldName} does not implement IDictionary.");
}
/// <summary>
/// 获取 CQRS dispatcher 运行时类型。
/// </summary>
private static Type GetDispatcherType()
{
return typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!;
}
/// <summary>
/// 消费整个异步流,确保建流路径被真实执行。
/// </summary>
private static async Task DrainAsync<T>(IAsyncEnumerable<T> stream)
{
await foreach (var _ in stream)
{
}
}
}
/// <summary>
/// 用于验证 request 服务类型缓存的测试请求。
/// </summary>
internal sealed record DispatcherCacheRequest : IRequest<int>;
/// <summary>
/// 用于验证 notification 服务类型缓存的测试通知。
/// </summary>
internal sealed record DispatcherCacheNotification : INotification;
/// <summary>
/// 用于验证 stream 服务类型缓存的测试请求。
/// </summary>
internal sealed record DispatcherCacheStreamRequest : IStreamRequest<int>;
/// <summary>
/// 用于验证 pipeline invoker 缓存的测试请求。
/// </summary>
internal sealed record DispatcherPipelineCacheRequest : IRequest<int>;
/// <summary>
/// 用于验证按响应类型分层 request invoker 缓存的测试请求。
/// </summary>
internal sealed record DispatcherStringCacheRequest : IRequest<string>;
/// <summary>
/// 处理 <see cref="DispatcherCacheRequest" />。
/// </summary>
internal sealed class DispatcherCacheRequestHandler : IRequestHandler<DispatcherCacheRequest, int>
{
/// <summary>
/// 返回固定结果,供缓存测试验证 dispatcher 请求路径。
/// </summary>
public ValueTask<int> Handle(DispatcherCacheRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(1);
}
}
/// <summary>
/// 处理 <see cref="DispatcherCacheNotification" />。
/// </summary>
internal sealed class DispatcherCacheNotificationHandler : INotificationHandler<DispatcherCacheNotification>
{
/// <summary>
/// 消费通知,不执行额外副作用。
/// </summary>
public ValueTask Handle(DispatcherCacheNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 处理 <see cref="DispatcherCacheStreamRequest" />。
/// </summary>
internal sealed class DispatcherCacheStreamHandler : IStreamRequestHandler<DispatcherCacheStreamRequest, int>
{
/// <summary>
/// 返回一个最小流,供缓存测试命中 stream 分发路径。
/// </summary>
public async IAsyncEnumerable<int> Handle(
DispatcherCacheStreamRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return 1;
await Task.CompletedTask;
}
}
/// <summary>
/// 处理 <see cref="DispatcherPipelineCacheRequest" />。
/// </summary>
internal sealed class DispatcherPipelineCacheRequestHandler : IRequestHandler<DispatcherPipelineCacheRequest, int>
{
/// <summary>
/// 返回固定结果,供 pipeline 缓存测试使用。
/// </summary>
public ValueTask<int> Handle(DispatcherPipelineCacheRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(2);
}
}
/// <summary>
/// 处理 <see cref="DispatcherStringCacheRequest" />。
/// </summary>
internal sealed class DispatcherStringCacheRequestHandler : IRequestHandler<DispatcherStringCacheRequest, string>
{
/// <summary>
/// 返回固定字符串,供按响应类型缓存测试验证 string 路径。
/// </summary>
public ValueTask<string> Handle(DispatcherStringCacheRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult("dispatcher-cache");
}
}
/// <summary>
/// 为 <see cref="DispatcherPipelineCacheRequest" /> 提供最小 pipeline 行为,
/// 用于命中 dispatcher 的 pipeline invoker 缓存分支。
/// </summary>
internal sealed class DispatcherPipelineCacheBehavior : IPipelineBehavior<DispatcherPipelineCacheRequest, int>
{
/// <summary>
/// 直接转发到下一个处理器。
/// </summary>
public ValueTask<int> Handle(
DispatcherPipelineCacheRequest request,
MessageHandlerDelegate<DispatcherPipelineCacheRequest, int> next,
CancellationToken cancellationToken)
{
return next(request, cancellationToken);
}
}

View File

@ -0,0 +1,492 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Tests.Logging;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 CQRS 处理器自动注册在顺序与容错层面的可观察行为。
/// </summary>
[TestFixture]
internal sealed class CqrsHandlerRegistrarTests
{
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
/// <summary>
/// 初始化测试容器并重置共享状态。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
DeterministicNotificationHandlerState.Reset();
_container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(
_container,
typeof(CqrsHandlerRegistrarTests).Assembly);
_container.Freeze();
_context = new ArchitectureContext(_container);
}
/// <summary>
/// 清理测试过程中创建的上下文与共享状态。
/// </summary>
[TearDown]
public void TearDown()
{
_context = null;
_container = null;
DeterministicNotificationHandlerState.Reset();
}
/// <summary>
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
/// </summary>
[Test]
public async Task PublishAsync_Should_Run_Notification_Handlers_In_Deterministic_Name_Order()
{
await _context!.PublishAsync(new DeterministicOrderNotification());
Assert.That(
DeterministicNotificationHandlerState.InvocationOrder,
Is.EqualTo(
[
nameof(AlphaDeterministicNotificationHandler),
nameof(ZetaDeterministicNotificationHandler)
]));
}
/// <summary>
/// 验证部分类型加载失败时仍能保留可加载类型,并记录诊断日志。
/// </summary>
[Test]
public void RegisterHandlers_Should_Register_Loadable_Types_And_Log_Warnings_When_Assembly_Load_Partially_Fails()
{
var originalProvider = LoggerFactoryResolver.Provider;
var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
var reflectionTypeLoadException = new ReflectionTypeLoadException(
[typeof(AlphaDeterministicNotificationHandler), null],
[new TypeLoadException("Missing optional dependency for registrar test.")]);
var partiallyLoadableAssembly = new Mock<Assembly>();
partiallyLoadableAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.PartiallyLoadableAssembly, Version=1.0.0.0");
partiallyLoadableAssembly
.Setup(static assembly => assembly.GetTypes())
.Throws(reflectionTypeLoadException);
LoggerFactoryResolver.Provider = capturingProvider;
try
{
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, partiallyLoadableAssembly.Object);
container.Freeze();
var handlers = container.GetAll<INotificationHandler<DeterministicOrderNotification>>();
var warningLogs = capturingProvider.Loggers
.SelectMany(static logger => logger.Logs)
.Where(static log => log.Level == LogLevel.Warning)
.ToList();
Assert.Multiple(() =>
{
Assert.That(
handlers.Select(static handler => handler.GetType()),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
Assert.That(warningLogs.Count, Is.GreaterThanOrEqualTo(2));
Assert.That(
warningLogs.Any(log => log.Message.Contains("partially failed", StringComparison.Ordinal)),
Is.True);
Assert.That(
warningLogs.Any(log =>
log.Message.Contains("Missing optional dependency", StringComparison.Ordinal)),
Is.True);
});
}
finally
{
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 验证当程序集提供源码生成的注册器时,运行时会优先使用该注册器而不是反射扫描类型列表。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Generated_Registry_When_Available()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.GeneratedRegistryAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedNotificationHandlerRegistry))]);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var handlers = container.GetAll<INotificationHandler<GeneratedRegistryNotification>>();
Assert.That(
handlers.Select(static handler => handler.GetType()),
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
}
/// <summary>
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
/// </summary>
[Test]
public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Invalid()
{
var originalProvider = LoggerFactoryResolver.Provider;
var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.InvalidGeneratedRegistryAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(string))]);
generatedAssembly
.Setup(static assembly => assembly.GetTypes())
.Returns([typeof(AlphaDeterministicNotificationHandler)]);
LoggerFactoryResolver.Provider = capturingProvider;
try
{
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
container.Freeze();
var handlers = container.GetAll<INotificationHandler<DeterministicOrderNotification>>();
var warningLogs = capturingProvider.Loggers
.SelectMany(static logger => logger.Logs)
.Where(static log => log.Level == LogLevel.Warning)
.ToList();
Assert.Multiple(() =>
{
Assert.That(
handlers.Select(static handler => handler.GetType()),
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
Assert.That(
warningLogs.Any(log =>
log.Message.Contains("does not implement", StringComparison.Ordinal)),
Is.True);
});
}
finally
{
LoggerFactoryResolver.Provider = originalProvider;
}
}
/// <summary>
/// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers
/// 而不是重新枚举整个程序集的类型列表。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Targeted_Type_Lookups_For_Reflection_Fallback_Without_Duplicates()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.PartialGeneratedRegistryAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
]);
generatedAssembly
.Setup(static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false))
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
var registrations = container.GetServicesUnsafe
.Where(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
descriptor.ImplementationType is not null)
.Select(static descriptor => descriptor.ImplementationType!)
.ToList();
Assert.That(
registrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Once);
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
}
/// <summary>
/// 验证手写 fallback metadata 直接提供 handler 类型时,运行时会复用这些类型,
/// 而不会再通过程序集名称查找或整程序集扫描补齐映射。
/// </summary>
[Test]
public void RegisterHandlers_Should_Use_Direct_Fallback_Types_Without_GetType_Or_GetTypes()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.Assembly.FullName);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType)
]);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
var registrations = container.GetServicesUnsafe
.Where(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
descriptor.ImplementationType is not null)
.Select(static descriptor => descriptor.ImplementationType!)
.ToList();
Assert.That(
registrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Never);
generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Never);
}
}
/// <summary>
/// 记录确定性通知处理器的实际执行顺序。
/// </summary>
internal static class DeterministicNotificationHandlerState
{
/// <summary>
/// 获取当前测试中的通知处理器执行顺序。
/// </summary>
public static List<string> InvocationOrder { get; } = [];
/// <summary>
/// 重置共享的执行顺序状态。
/// </summary>
public static void Reset()
{
InvocationOrder.Clear();
}
}
/// <summary>
/// 用于验证同一通知的多个处理器是否按稳定顺序执行。
/// </summary>
internal sealed record DeterministicOrderNotification : INotification;
/// <summary>
/// 故意放在 Alpha 之前声明,用于验证注册器不会依赖源码声明顺序。
/// </summary>
internal sealed class ZetaDeterministicNotificationHandler : INotificationHandler<DeterministicOrderNotification>
{
/// <summary>
/// 记录当前处理器已执行。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken)
{
DeterministicNotificationHandlerState.InvocationOrder.Add(nameof(ZetaDeterministicNotificationHandler));
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 名称排序上应先于 Zeta 处理器执行的通知处理器。
/// </summary>
internal sealed class AlphaDeterministicNotificationHandler : INotificationHandler<DeterministicOrderNotification>
{
/// <summary>
/// 记录当前处理器已执行。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken)
{
DeterministicNotificationHandlerState.InvocationOrder.Add(nameof(AlphaDeterministicNotificationHandler));
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 为 CQRS 注册测试捕获真实启动路径中创建的日志记录器。
/// </summary>
/// <remarks>
/// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。
/// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。
/// </remarks>
internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
{
private readonly List<TestLogger> _loggers = [];
/// <summary>
/// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。
/// </summary>
/// <param name="minLevel">要应用到新建测试日志器的最小日志级别。</param>
public CapturingLoggerFactoryProvider(LogLevel minLevel = LogLevel.Info)
{
MinLevel = minLevel;
}
/// <summary>
/// 获取通过当前提供程序创建的全部测试日志器。
/// </summary>
public IReadOnlyList<TestLogger> Loggers => _loggers;
/// <summary>
/// 获取或设置新建测试日志器的最小日志级别。
/// </summary>
public LogLevel MinLevel { get; set; }
/// <summary>
/// 创建一个测试日志器并将其纳入捕获集合。
/// </summary>
/// <param name="name">日志记录器名称。</param>
/// <returns>用于后续断言的测试日志器。</returns>
public ILogger CreateLogger(string name)
{
var logger = new TestLogger(name, MinLevel);
_loggers.Add(logger);
return logger;
}
}
/// <summary>
/// 用于验证生成注册器路径的通知消息。
/// </summary>
internal sealed record GeneratedRegistryNotification : INotification;
/// <summary>
/// 由模拟的源码生成注册器显式注册的通知处理器。
/// </summary>
internal sealed class GeneratedRegistryNotificationHandler : INotificationHandler<GeneratedRegistryNotification>
{
/// <summary>
/// 处理生成注册器测试中的通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 模拟源码生成器为某个程序集生成的 CQRS 处理器注册器。
/// </summary>
internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将测试通知处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}
/// <summary>
/// 用于验证“生成注册器 + reflection fallback”组合路径的私有嵌套处理器容器。
/// </summary>
internal sealed class ReflectionFallbackNotificationContainer
{
/// <summary>
/// 获取仅能通过反射补扫接入的私有嵌套处理器类型。
/// </summary>
public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler);
private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler
: INotificationHandler<GeneratedRegistryNotification>
{
/// <summary>
/// 处理测试通知。
/// </summary>
/// <param name="notification">通知实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
}
/// <summary>
/// 模拟局部生成注册器场景中,仅注册“可由生成代码直接引用”的那部分 handlers。
/// </summary>
internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 将生成路径可见的通知处理器注册到目标服务集合。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
logger.Debug(
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
}
}

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,12 +1,10 @@
using System.Diagnostics;
using System.Reflection;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Mediator;
namespace GFramework.Cqrs.Tests.Mediator;
/// <summary>
/// Mediator高级特性专项测试
@ -20,17 +18,17 @@ public class MediatorAdvancedFeaturesTests
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
TestCircuitBreakerHandler.Reset();
var loggerField = typeof(MicrosoftDiContainer).GetField("_logger",
BindingFlags.NonPublic | BindingFlags.Instance);
loggerField?.SetValue(_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(MediatorAdvancedFeaturesTests)));
// 注册Mediator及相关处理器
_container.ExecuteServicesHook(configurator =>
{
configurator.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; });
});
CqrsTestRuntime.RegisterHandlers(
_container,
typeof(MediatorAdvancedFeaturesTests).Assembly,
typeof(ArchitectureContext).Assembly);
_container.Freeze();
_context = new ArchitectureContext(_container);
@ -43,9 +41,10 @@ public class MediatorAdvancedFeaturesTests
_container = null;
}
private ArchitectureContext? _context;
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
[Test]
public async Task Request_With_Validation_Behavior_Should_Validate_Input()
@ -136,9 +135,6 @@ public class MediatorAdvancedFeaturesTests
[Test]
public async Task Circuit_Breaker_Should_Prevent_Cascading_Failures()
{
TestCircuitBreakerHandler.FailureCount = 0;
TestCircuitBreakerHandler.SuccessCount = 0;
// 先触发几次失败
for (int i = 0; i < 5; i++)
{
@ -276,12 +272,10 @@ public sealed class TestTransientErrorRequestHandler : IRequestHandler<TestTrans
public sealed class TestCircuitBreakerRequestHandler : IRequestHandler<TestCircuitBreakerRequest, string>
{
private static bool _circuitOpen = false;
public ValueTask<string> Handle(TestCircuitBreakerRequest request, CancellationToken cancellationToken)
{
// 检查断路器状态
if (_circuitOpen)
if (TestCircuitBreakerHandler.CircuitOpen)
{
throw new InvalidOperationException("Circuit breaker is open");
}
@ -293,7 +287,7 @@ public sealed class TestCircuitBreakerRequestHandler : IRequestHandler<TestCircu
// 达到阈值后打开断路器
if (TestCircuitBreakerHandler.FailureCount >= 5)
{
_circuitOpen = true;
TestCircuitBreakerHandler.CircuitOpen = true;
}
throw new InvalidOperationException("Service unavailable");
@ -452,6 +446,17 @@ public static class TestCircuitBreakerHandler
{
public static int FailureCount { get; set; }
public static int SuccessCount { get; set; }
public static bool CircuitOpen { get; set; }
/// <summary>
/// 重置断路器测试状态,避免静态字段在测试之间互相污染。
/// </summary>
public static void Reset()
{
FailureCount = 0;
SuccessCount = 0;
CircuitOpen = false;
}
}
public sealed record TestCircuitBreakerRequest : IRequest<string>
@ -487,4 +492,4 @@ public sealed record TestDatabaseRequest : IRequest<string>
public List<string> Storage { get; init; } = new();
}
#endregion
#endregion

View File

@ -1,15 +1,14 @@
using System.Diagnostics;
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Command;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
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与架构上下文集成测试
@ -23,6 +22,7 @@ public class MediatorArchitectureIntegrationTests
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
TestPerDispatchContextAwareHandler.Reset();
var loggerField = typeof(MicrosoftDiContainer).GetField("_logger",
BindingFlags.NonPublic | BindingFlags.Instance);
@ -33,11 +33,10 @@ public class MediatorArchitectureIntegrationTests
_commandBus = new CommandExecutor();
_container.RegisterPlurality(_commandBus);
// 注册Mediator
_container.ExecuteServicesHook(configurator =>
{
configurator.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; });
});
CqrsTestRuntime.RegisterHandlers(
_container,
typeof(MediatorArchitectureIntegrationTests).Assembly,
typeof(ArchitectureContext).Assembly);
_container.Freeze();
_context = new ArchitectureContext(_container);
@ -51,9 +50,10 @@ public class MediatorArchitectureIntegrationTests
_commandBus = null;
}
private ArchitectureContext? _context;
private MicrosoftDiContainer? _container;
private CommandExecutor? _commandBus;
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
[Test]
public async Task Handler_Can_Access_Architecture_Context()
@ -292,6 +292,20 @@ public class MediatorArchitectureIntegrationTests
Assert.That(traditionalCommand.Executed, Is.True);
Assert.That(result, Is.EqualTo(42));
}
[Test]
public async Task ContextAware_Handler_Should_Use_A_Fresh_Instance_Per_Request()
{
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest());
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest());
Assert.Multiple(() =>
{
Assert.That(firstResult, Is.Not.EqualTo(secondResult));
Assert.That(TestPerDispatchContextAwareHandler.SeenInstanceIds, Is.EqualTo([firstResult, secondResult]));
Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context));
});
}
}
#region Integration Test Classes
@ -445,6 +459,42 @@ public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorReq
}
}
/// <summary>
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。
/// </summary>
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
IRequestHandler<TestPerDispatchContextAwareRequest, int>
{
private static int _nextInstanceId;
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
public static List<IArchitectureContext?> Contexts { get; } = [];
public static List<int> SeenInstanceIds { get; } = [];
/// <summary>
/// 记录当前实例编号与收到的架构上下文。
/// </summary>
/// <param name="request">请求实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>当前处理器实例编号。</returns>
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
{
Contexts.Add(Context);
SeenInstanceIds.Add(_instanceId);
return ValueTask.FromResult(_instanceId);
}
/// <summary>
/// 重置跨测试共享的实例跟踪状态。
/// </summary>
public static void Reset()
{
Contexts.Clear();
SeenInstanceIds.Clear();
_nextInstanceId = 0;
}
}
public sealed record TestContextAwareRequest : IRequest<string>;
public static class TestContextAwareHandler
@ -545,6 +595,11 @@ public sealed record TestMediatorRequest : IRequest<int>
public int Value { get; init; }
}
/// <summary>
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
/// </summary>
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
// 传统命令用于混合测试
public class TestTraditionalCommand : ICommand
{
@ -559,4 +614,4 @@ public class TestTraditionalCommand : ICommand
public IArchitectureContext GetContext() => null!;
}
#endregion
#endregion

View File

@ -1,8 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
using GFramework.Core.Command;
using GFramework.Core.Environment;
@ -10,22 +8,17 @@ using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Query;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
using GFramework.Cqrs.Abstractions.Cqrs;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
// ✅ Mediator 库的命名空间
// ✅ 使用 global using 或别名来区分
namespace GFramework.Core.Tests.Mediator;
namespace GFramework.Cqrs.Tests.Mediator;
[TestFixture]
public class MediatorComprehensiveTests
{
/// <summary>
/// 测试初始化方法,在每个测试方法执行前运行。
/// 负责初始化日志工厂、依赖注入容器、Mediator以及各种总线服务。
/// 负责初始化日志工厂、依赖注入容器、自有 CQRS 处理器以及各种总线服务。
/// </summary>
[SetUp]
public void SetUp()
@ -51,13 +44,11 @@ public class MediatorComprehensiveTests
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
// ✅ 注册 Mediator
_container.ExecuteServicesHook(configurator =>
{
configurator.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; });
});
CqrsTestRuntime.RegisterHandlers(
_container,
typeof(MediatorComprehensiveTests).Assembly,
typeof(ArchitectureContext).Assembly);
// ✅ Freeze 容器
_container.Freeze();
_context = new ArchitectureContext(_container);
@ -79,13 +70,14 @@ public class MediatorComprehensiveTests
_environment = null;
}
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>
/// 测试SendRequestAsync方法在请求有效时返回结果
@ -194,19 +186,19 @@ public class MediatorComprehensiveTests
/// <summary>
/// 测试未注册的Mediator抛出InvalidOperationException
/// 测试未注册的 CQRS handler 时抛出 InvalidOperationException
/// </summary>
[Test]
public void Unregistered_Mediator_Should_Throw_InvalidOperationException()
public void Unregistered_Cqrs_Handler_Should_Throw_InvalidOperationException()
{
var containerWithoutMediator = new MicrosoftDiContainer();
containerWithoutMediator.Freeze();
var containerWithoutHandlers = new MicrosoftDiContainer();
containerWithoutHandlers.Freeze();
var contextWithoutMediator = new ArchitectureContext(containerWithoutMediator);
var contextWithoutHandlers = new ArchitectureContext(containerWithoutHandlers);
var testRequest = new TestRequest { Value = 42 };
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await contextWithoutMediator.SendRequestAsync(testRequest));
await contextWithoutHandlers.SendRequestAsync(testRequest));
}
/// <summary>
@ -270,10 +262,10 @@ public class MediatorComprehensiveTests
}
/// <summary>
/// 测试并发Mediator请求不会相互干扰
/// 测试并发 CQRS 请求不会相互干扰
/// </summary>
[Test]
public async Task Concurrent_Mediator_Requests_Should_Not_Interfere()
public async Task Concurrent_Cqrs_Requests_Should_Not_Interfere()
{
const int requestCount = 10;
var tasks = new List<Task<int>>();
@ -389,10 +381,10 @@ public class MediatorComprehensiveTests
}
/// <summary>
/// 测试Mediator性能基准
/// 测试 CQRS 性能基准
/// </summary>
[Test]
public async Task Performance_Benchmark_For_Mediator()
public async Task Performance_Benchmark_For_Cqrs()
{
const int iterations = 1000;
var stopwatch = Stopwatch.StartNew();
@ -413,17 +405,17 @@ public class MediatorComprehensiveTests
}
/// <summary>
/// 测试Mediator和传统CQRS可以共存
/// 测试自有 CQRS 和传统 CQRS 可以共存
/// </summary>
[Test]
public async Task Mediator_And_Legacy_CQRS_Can_Coexist()
public async Task Cqrs_And_Legacy_CQRS_Can_Coexist()
{
// 使用传统方式
var legacyCommand = new TestLegacyCommand();
_context!.SendCommand(legacyCommand);
Assert.That(legacyCommand.Executed, Is.True);
// 使用Mediator方式
// 使用自有 CQRS 方式
var mediatorCommand = new TestCommandWithResult { ResultValue = 999 };
var result = await _context.SendAsync(mediatorCommand);
Assert.That(result, Is.EqualTo(999));
@ -434,7 +426,7 @@ public class MediatorComprehensiveTests
}
}
#region Advanced Test Classes for Mediator Features
#region Advanced Test Classes for CQRS Features
public sealed record TestLongRunningRequest : IRequest<string>
{
@ -628,9 +620,9 @@ public class TestLegacyCommand : ICommand
#endregion
#region Test Classes - Mediator ()
#region Test Classes - CQRS Runtime
// ✅ 这些类使用 Mediator.IRequest
// ✅ 这些类使用自有 CQRS IRequest
public sealed record TestRequest : IRequest<int>
{
public int Value { get; init; }
@ -662,7 +654,7 @@ public sealed record TestStreamRequest : IStreamRequest<int>
public int[] Values { get; init; } = [];
}
// ✅ 这些 Handler 使用 Mediator.IRequestHandler
// ✅ 这些 Handler 使用自有 CQRS IRequestHandler
public sealed class TestRequestHandler : IRequestHandler<TestRequest, int>
{
public ValueTask<int> Handle(TestRequest request, CancellationToken cancellationToken)
@ -726,4 +718,4 @@ public sealed class TestStreamRequestHandler : IStreamRequestHandler<TestStreamR
}
}
#endregion
#endregion

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