diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 8cb624ec..5fc95d9d 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -16,6 +16,8 @@ reviews: auto_review: enabled: true drafts: false # draft PR 不 review + base_branches: + - refactor/cqrs-architecture-decoupling chat: auto_reply: true diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 45b9b112..9588c304 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -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" \ No newline at end of file + git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2b53c25..defe6c71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 \ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1802de8f..0a068113 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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 * * *' diff --git a/AGENTS.md b/AGENTS.md index c63a2bfa..720b53af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 570606c7..da7e13c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 的一致性与可维护性。 diff --git a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs index 0055d3fe..e1687ff9 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitecture.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitecture.cs @@ -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(Action? onCreated = null) where T : class, IUtility; /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, /// 也支持绑定到单一请求/响应对的封闭行为类型。 /// /// 行为类型,必须是引用类型 + void RegisterCqrsPipelineBehavior() + where TBehavior : class; + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 + /// 既支持实现 IPipelineBehavior<,> 的开放泛型行为类型, + /// 也支持绑定到单一请求/响应对的封闭行为类型。 + /// + /// 行为类型,必须是引用类型 + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] void RegisterMediatorBehavior() where TBehavior : class; + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 + void RegisterCqrsHandlersFromAssembly(Assembly assembly); + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。 + /// + /// 要接入的程序集集合。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 + void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); + /// /// 安装架构模块 /// @@ -101,4 +133,4 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia /// /// 表示异步等待操作的任务 Task WaitUntilReadyAsync(); -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs index 3469d8db..2e8894bc 100644 --- a/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs @@ -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; /// -/// 架构上下文接口,提供对系统、模型、工具类的访问以及命令、查询、事件的发送和注册功能 +/// 架构上下文接口,统一暴露框架组件访问、兼容旧命令/查询总线,以及当前推荐的 CQRS 运行时入口。 /// -public interface IArchitectureContext +/// +/// 旧的 GFramework.Core.Abstractions.CommandGFramework.Core.Abstractions.Query 契约会继续通过原有 Command/Query Executor 路径执行,以保证存量代码兼容。 +/// 新的 GFramework.Cqrs.Abstractions.Cqrs 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。 +/// 新功能优先使用 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。 +/// +public interface IArchitectureContext : ICqrsContext { /// /// 获取指定类型的服务实例 @@ -104,87 +109,92 @@ public interface IArchitectureContext IReadOnlyList GetUtilitiesByPriority() where TUtility : class, IUtility; /// - /// 发送一个命令 + /// 发送一个旧版命令。 /// - /// 要发送的命令 + /// 要发送的旧版命令。 void SendCommand(ICommand command); /// - /// 发送一个带返回值的命令 + /// 发送一个旧版带返回值命令。 /// - /// 命令执行结果类型 - /// 要发送的命令 - /// 命令执行结果 - TResult SendCommand(Command.ICommand command); + /// 命令执行结果类型。 + /// 要发送的旧版命令。 + /// 命令执行结果。 + TResult SendCommand(ICommand command); /// - /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// 发送一个新版 CQRS 命令并返回结果。 /// - /// 命令响应类型 - /// 要发送的命令对象 - /// 命令执行结果 - TResponse SendCommand(Mediator.ICommand command); + /// 命令响应类型。 + /// 要发送的 CQRS 命令。 + /// 命令执行结果。 + /// + /// 这是迁移后的推荐命令入口。无返回值命令应实现 IRequest<Unit>,并优先通过 调用。 + /// + TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command); /// - /// 发送并异步执行一个命令 + /// 异步发送一个旧版命令。 /// - /// 要发送的命令 + /// 要发送的旧版命令。 Task SendCommandAsync(IAsyncCommand command); /// - /// [Mediator] 异步发送命令并返回结果 - /// 通过Mediator模式发送命令请求,支持取消操作 + /// 异步发送一个新版 CQRS 命令并返回结果。 /// - /// 命令响应类型 - /// 要发送的命令对象 - /// 取消令牌,用于取消操作 - /// 包含命令执行结果的ValueTask - ValueTask SendCommandAsync(Mediator.ICommand command, + /// 命令响应类型。 + /// 要发送的 CQRS 命令。 + /// 取消令牌。 + /// 包含命令执行结果的值任务。 + ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default); /// - /// 发送并异步执行一个带返回值的命令 + /// 异步发送一个旧版带返回值命令。 /// - /// 命令执行结果类型 - /// 要发送的命令 - /// 命令执行结果 + /// 命令执行结果类型。 + /// 要发送的旧版命令。 + /// 命令执行结果。 Task SendCommandAsync(IAsyncCommand command); /// - /// 发送一个查询请求 + /// 发送一个旧版查询请求。 /// - /// 查询结果类型 - /// 要发送的查询 - /// 查询结果 - TResult SendQuery(Query.IQuery query); + /// 查询结果类型。 + /// 要发送的旧版查询。 + /// 查询结果。 + TResult SendQuery(IQuery query); /// - /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// 发送一个新版 CQRS 查询并返回结果。 /// - /// 查询响应类型 - /// 要发送的查询对象 - /// 查询结果 - TResponse SendQuery(Mediator.IQuery query); + /// 查询响应类型。 + /// 要发送的 CQRS 查询。 + /// 查询结果。 + /// + /// 这是迁移后的推荐查询入口。新查询应优先实现 GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse>。 + /// + TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query); /// - /// 异步发送一个查询请求 + /// 异步发送一个旧版查询请求。 /// - /// 查询结果类型 - /// 要发送的异步查询 - /// 查询结果 + /// 查询结果类型。 + /// 要发送的旧版异步查询。 + /// 查询结果。 Task SendQueryAsync(IAsyncQuery query); /// - /// [Mediator] 异步发送查询并返回结果 - /// 通过Mediator模式发送查询请求,支持取消操作 + /// 异步发送一个新版 CQRS 查询并返回结果。 /// - /// 查询响应类型 - /// 要发送的查询对象 - /// 取消令牌,用于取消操作 - /// 包含查询结果的ValueTask - ValueTask SendQueryAsync(Mediator.IQuery query, + /// 查询响应类型。 + /// 要发送的 CQRS 查询。 + /// 取消令牌。 + /// 包含查询结果的值任务。 + ValueTask SendQueryAsync(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default); /// @@ -216,28 +226,40 @@ public interface IArchitectureContext void UnRegisterEvent(Action onEvent); /// - /// 发送请求(统一处理 Command/Query) + /// 发送新版 CQRS 请求,并统一处理命令与查询。 /// + /// + /// 这是自有 CQRS 运行时的主入口。新代码应优先通过该方法或 进入 dispatcher。 + /// ValueTask SendRequestAsync( IRequest request, CancellationToken cancellationToken = default); /// - /// 发送请求(同步版本,不推荐) + /// 发送新版 CQRS 请求的同步包装版本。 /// + /// + /// 仅为兼容同步调用链保留;新代码应优先使用异步入口,避免阻塞当前线程。 + /// TResponse SendRequest(IRequest request); /// - /// 发布通知(一对多事件) + /// 发布新版 CQRS 通知。 /// + /// + /// 该入口用于一对多通知分发,与框架级 EventBus 事件系统并存,适合围绕请求处理过程传播领域通知。 + /// ValueTask PublishAsync( TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification; /// - /// 创建流式请求(用于大数据集) + /// 创建新版 CQRS 流式请求。 /// + /// + /// 适用于需要按序惰性产出大量结果的场景。调用方应消费返回的异步序列,而不是回退到旧版查询总线。 + /// IAsyncEnumerable CreateStream( IStreamRequest request, CancellationToken cancellationToken = default); @@ -245,7 +267,7 @@ public interface IArchitectureContext // === 便捷扩展方法 === /// - /// 发送命令(无返回值) + /// 发送一个无返回值的新版 CQRS 命令。 /// ValueTask SendAsync( TCommand command, @@ -253,7 +275,7 @@ public interface IArchitectureContext where TCommand : IRequest; /// - /// 发送命令(有返回值) + /// 发送一个有返回值的新版 CQRS 请求。 /// ValueTask SendAsync( IRequest command, @@ -265,4 +287,4 @@ public interface IArchitectureContext /// /// 环境对象实例 IEnvironment GetEnvironment(); -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs new file mode 100644 index 00000000..5bcbf862 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 提供旧 GFramework.Core.Abstractions.Cqrs 命名空间下的 CQRS runtime 兼容别名。 +/// +/// +/// 正式 runtime seam 已迁移到 , +/// 但当前仍保留该接口以避免立即打断历史公开路径与既有二进制引用。 +/// 新代码应优先依赖 GFramework.Cqrs.Abstractions.Cqrs 下的正式契约。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface ICqrsRuntime : GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime +{ +} diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index 8ec77b72..7a70b332 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -17,6 +17,9 @@ + + + all @@ -26,6 +29,6 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/GFramework.Core.Abstractions/GlobalUsings.cs b/GFramework.Core.Abstractions/GlobalUsings.cs index 7c0f226a..204433de 100644 --- a/GFramework.Core.Abstractions/GlobalUsings.cs +++ b/GFramework.Core.Abstractions/GlobalUsings.cs @@ -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; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index b61d0f3a..3149b3c4 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -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(Func factory) where TService : class; /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑 + /// 注册 CQRS 请求管道行为。 /// /// 行为类型,必须是引用类型 + void RegisterCqrsPipelineBehavior() + where TBehavior : class; + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 + /// + /// 行为类型,必须是引用类型 + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] void RegisterMediatorBehavior() where TBehavior : class; + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。 + /// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 + void RegisterCqrsHandlersFromAssembly(Assembly assembly); + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。 + /// + /// 要接入的程序集集合。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 + void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies); + /// /// 配置服务 @@ -227,4 +258,4 @@ public interface IIocContainer : IContextAware IServiceScope CreateScope(); #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs new file mode 100644 index 00000000..fdffe5b2 --- /dev/null +++ b/GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs @@ -0,0 +1,281 @@ +namespace GFramework.Core.Abstractions.Logging; + +/// +/// 提供全局日志工厂访问入口。 +/// +/// +/// 该类型位于抽象层,是为了让上层模块可以在不依赖 GFramework.Core 实现程序集的前提下 +/// 获取日志记录器。默认 provider 会优先通过反射解析 GFramework.Core 中的控制台实现, +/// 若宿主未加载该程序集,则退回到静默 provider,避免抽象层形成实现层循环依赖。 +/// +public static class LoggerFactoryResolver +{ + private static readonly object ProviderLock = new(); + + private static string DefaultProviderTypeName = + "GFramework.Core.Logging.ConsoleLoggerFactoryProvider, GFramework.Core"; + + private static ILoggerFactoryProvider? _provider; + + /// + /// 获取或设置当前日志工厂提供程序。 + /// + /// + /// 读取与赋值都会通过同一把锁串行化,确保并发调用方观察到确定的 provider 引用。 + /// 当调用方未显式赋值时,会在首次访问时尝试解析默认实现;若解析失败,则退回静默 provider。 + /// + /// + /// 当赋值为 时抛出。 + /// + public static ILoggerFactoryProvider Provider + { + get + { + lock (ProviderLock) + { + _provider ??= CreateDefaultProvider(); + return _provider; + } + } + set + { + var provider = value ?? throw new ArgumentNullException(nameof(value)); + + lock (ProviderLock) + { + _provider = provider; + } + } + } + + /// + /// 获取或设置新创建日志记录器的最小日志级别。 + /// + /// + /// 该属性直接代理到当前 ,确保调用方调整级别后立即影响后续创建的日志器。 + /// + 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(); + } + + /// + /// 当宿主未提供默认日志实现时使用的静默 provider。 + /// + private sealed class SilentLoggerFactoryProvider : ILoggerFactoryProvider + { + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + public ILogger CreateLogger(string name) + { + return new SilentLogger(name); + } + } + + /// + /// 默认日志实现不可用时的 no-op 日志器。 + /// + 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) + { + } + } +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs new file mode 100644 index 00000000..ddec08c8 --- /dev/null +++ b/GFramework.Core.Tests/Architectures/ArchitectureAdditionalCqrsHandlersTests.cs @@ -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; + +/// +/// 验证架构初始化阶段可以显式接入默认程序集之外的 CQRS handlers。 +/// +[TestFixture] +public sealed class ArchitectureAdditionalCqrsHandlersTests +{ + /// + /// 初始化日志工厂和共享测试状态。 + /// + [SetUp] + public void SetUp() + { + _previousLoggerFactoryProvider = LoggerFactoryResolver.Provider; + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + GameContext.Clear(); + AdditionalAssemblyNotificationHandlerState.Reset(); + } + + /// + /// 清理测试过程中写入的共享状态。 + /// + [TearDown] + public void TearDown() + { + AdditionalAssemblyNotificationHandlerState.Reset(); + GameContext.Clear(); + LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider + ?? throw new InvalidOperationException( + "LoggerFactoryResolver.Provider should be captured during setup."); + } + + private ILoggerFactoryProvider? _previousLoggerFactoryProvider; + + /// + /// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。 + /// + /// The asynchronous test task. + [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(); + } + } + + /// + /// 验证不同 实例只要解析到相同程序集键,就不会向容器重复写入相同 handler 映射。 + /// + /// The asynchronous test task. + [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(); + } + } + + /// + /// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。 + /// 该测试替身模拟“扩展程序集已经挂接 source-generator,运行时只需显式接入该程序集”的真实路径。 + /// + /// 包含程序集级 handler registry 元数据的 mocked Assembly。 + private static Mock CreateGeneratedHandlerAssembly() + { + var generatedAssembly = new Mock(); + 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; + } + + /// + /// 创建复用现有测试架构基建的测试架构,并在注册阶段后执行额外程序集接入逻辑。 + /// + /// 初始化阶段执行的额外 CQRS 程序集接入逻辑。 + /// 带有注册后钩子的测试架构实例。 + private static SyncTestArchitecture CreateArchitecture(Action configure) + { + var architecture = new SyncTestArchitecture(); + architecture.AddPostRegistrationHook(configure); + return architecture; + } +} + +/// +/// 用于验证额外程序集接入是否成功的测试通知。 +/// +public sealed record AdditionalAssemblyNotification : INotification; + +/// +/// 记录模拟扩展程序集通知处理器的执行次数。 +/// +public static class AdditionalAssemblyNotificationHandlerState +{ + private static int _invocationCount; + + /// + /// 获取当前测试进程中该处理器的执行次数。 + /// + /// + /// 该计数器通过原子读写维护,以支持 NUnit 并行执行环境中的并发访问。 + /// + public static int InvocationCount => Volatile.Read(ref _invocationCount); + + /// + /// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。 + /// + public static void RecordInvocation() + { + Interlocked.Increment(ref _invocationCount); + } + + /// + /// 清理共享计数器,避免测试间相互污染。 + /// + public static void Reset() + { + Interlocked.Exchange(ref _invocationCount, 0); + } +} + +/// +/// 模拟由 source-generator 为扩展程序集生成的 CQRS handler registry。 +/// +internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将扩展程序集中的通知处理器映射写入服务集合。 + /// + /// 目标服务集合。 + /// 日志记录器。 + /// + /// 当 时抛出。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient>(_ => CreateHandler()); + logger.Debug( + $"Registered CQRS handler proxy for {typeof(INotificationHandler).FullName}."); + } + + /// + /// 创建一个仅供显式程序集注册路径使用的动态通知处理器。 + /// + /// 用于记录通知触发次数的测试替身处理器。 + private static INotificationHandler CreateHandler() + { + var handler = new Mock>(); + handler + .Setup(target => target.Handle(It.IsAny(), It.IsAny())) + .Returns(() => + { + AdditionalAssemblyNotificationHandlerState.RecordInvocation(); + return ValueTask.CompletedTask; + }); + return handler.Object; + } +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs index 583c30c8..f6cbdafa 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureComponentRegistryBehaviorTests.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs index 00684cb9..584090ab 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs @@ -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; /// /// 测试构造函数在所有参数都有效时不应抛出异常 @@ -298,6 +302,76 @@ public class ArchitectureContextTests Assert.That(environment, Is.Not.Null); Assert.That(environment, Is.InstanceOf()); } + + /// + /// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。 + /// + [Test] + public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently() + { + const int workerCount = 8; + var workerStartupTimeout = TimeSpan.FromSeconds(5); + var firstResolutionTimeout = TimeSpan.FromSeconds(5); + using var startGate = new ManualResetEventSlim(false); + using var allowResolutionToComplete = new ManualResetEventSlim(false); + using var workersReady = new CountdownEvent(workerCount); + var resolutionCallCount = 0; + var runtime = new Mock(MockBehavior.Strict); + var container = new Mock(MockBehavior.Strict); + + runtime.Setup(mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new ValueTask(42)); + + container.Setup(mockContainer => mockContainer.Get()) + .Returns(() => + { + Interlocked.Increment(ref resolutionCallCount); + allowResolutionToComplete.Wait(); + return runtime.Object; + }); + + var context = new ArchitectureContext(container.Object); + var requests = Enumerable.Range(0, workerCount) + .Select(_ => Task.Run(async () => + { + workersReady.Signal(); + startGate.Wait(); + return await context.SendRequestAsync(new TestCqrsRequest()); + })) + .ToArray(); + + Assert.That( + workersReady.Wait(workerStartupTimeout), + Is.True, + "Expected all workers to be ready before releasing start gate."); + startGate.Set(); + + Assert.That( + SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, firstResolutionTimeout), + Is.True, + "Expected at least one CQRS runtime resolution attempt."); + + allowResolutionToComplete.Set(); + + var responses = await Task.WhenAll(requests); + + Assert.That(responses, Has.All.EqualTo(42)); + Assert.That(resolutionCallCount, Is.EqualTo(1)); + container.Verify(mockContainer => mockContainer.Get(), Times.Once); + runtime.Verify( + mockRuntime => mockRuntime.SendAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Exactly(requests.Length)); + } + + private sealed class TestCqrsRequest : IRequest + { + } } #region Test Classes @@ -442,4 +516,4 @@ public class TestEventV2 public int Data { get; init; } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs index 717e1b18..10945ad2 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureInitializationPipelineTests.cs @@ -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 { } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs index b0ef7262..943e2bfd 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs index 05601230..493f3590 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs @@ -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; /// -/// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 Mediator 行为注册能力。 -/// 这些测试覆盖模块安装回调和中介管道行为接入,确保模块管理器仍然保持可观察行为不变。 +/// 验证 Architecture 通过 ArchitectureModules 暴露出的模块安装与 CQRS 行为注册能力。 +/// 这些测试覆盖模块安装回调和请求管道行为接入,确保模块管理器仍然保持可观察行为不变。 /// [TestFixture] public class ArchitectureModulesBehaviorTests @@ -57,7 +57,29 @@ public class ArchitectureModulesBehaviorTests } /// - /// 验证注册的 Mediator 行为会参与请求管道执行。 + /// 验证注册的 CQRS 行为会参与请求管道执行。 + /// + [Test] + public async Task RegisterCqrsPipelineBehavior_Should_Apply_Pipeline_Behavior_To_Request() + { + var architecture = new ModuleTestArchitecture(target => + target.RegisterCqrsPipelineBehavior>()); + + await architecture.InitializeAsync(); + + var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest()); + + Assert.Multiple(() => + { + Assert.That(response, Is.EqualTo("handled")); + Assert.That(TrackingPipelineBehavior.InvocationCount, Is.EqualTo(1)); + }); + + await architecture.DestroyAsync(); + } + + /// + /// 验证兼容别名 RegisterMediatorBehavior 仍会把 CQRS 行为接入请求管道。 /// [Test] public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request() @@ -83,12 +105,6 @@ public class ArchitectureModulesBehaviorTests /// private sealed class ModuleTestArchitecture(Action registrationAction) : Architecture { - /// - /// 打开 Mediator 服务注册,以便测试中介行为接入。 - /// - public override Action? Configurator => - services => services.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; }); - /// /// 在初始化阶段执行测试注入的模块注册逻辑。 /// @@ -178,11 +194,10 @@ public sealed class TrackingPipelineBehavior : IPipelineBeh /// 取消令牌。 /// 下游处理器的响应结果。 public async ValueTask Handle( - TRequest message, - MessageHandlerDelegate next, + TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) { InvocationCount++; return await next(message, cancellationToken); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs index 93244298..52c2ecbd 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureServicesTests.cs @@ -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 SendCommandAsync(global::Mediator.ICommand command, + /// + /// 测试桩:异步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令响应任务。 + /// 该测试桩未实现此成员。 + public ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(global::Mediator.ICommand command) + /// + /// 测试桩:同步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 命令响应。 + /// 该测试桩未实现此成员。 + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync(global::Mediator.IQuery query, + /// + /// 测试桩:异步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 取消令牌。 + /// 查询结果任务。 + /// 该测试桩未实现此成员。 + public ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(global::Mediator.IQuery query) + /// + /// 测试桩:同步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 查询结果。 + /// 该测试桩未实现此成员。 + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } @@ -386,7 +419,8 @@ public class TestArchitectureContextV3 : IArchitectureContext throw new NotImplementedException(); } - public IAsyncEnumerable CreateStream(IStreamRequest request, + public IAsyncEnumerable CreateStream( + IStreamRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -439,4 +473,4 @@ public class TestArchitectureContextV3 : IArchitectureContext } } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Architectures/GameContextTests.cs b/GFramework.Core.Tests/Architectures/GameContextTests.cs index 8f1bea57..9b990e78 100644 --- a/GFramework.Core.Tests/Architectures/GameContextTests.cs +++ b/GFramework.Core.Tests/Architectures/GameContextTests.cs @@ -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 { } + /// + /// 测试桩:异步发送统一 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendRequestAsync(IRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:同步发送统一 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 请求响应。 + /// 该测试桩未实现此成员。 public TResponse SendRequest(IRequest request) { throw new NotImplementedException(); } - public ValueTask SendCommandAsync(global::Mediator.ICommand command, + /// + /// 测试桩:异步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令响应任务。 + /// 该测试桩未实现此成员。 + public ValueTask SendCommandAsync( + GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendCommand(global::Mediator.ICommand command) + /// + /// 测试桩:同步发送 CQRS 命令并返回响应。 + /// + /// 命令响应类型。 + /// 要发送的命令。 + /// 命令响应。 + /// 该测试桩未实现此成员。 + public TResponse SendCommand(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand command) { throw new NotImplementedException(); } - public ValueTask SendQueryAsync(global::Mediator.IQuery query, + /// + /// 测试桩:异步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 取消令牌。 + /// 查询结果任务。 + /// 该测试桩未实现此成员。 + public ValueTask SendQueryAsync( + GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public TResponse SendQuery(global::Mediator.IQuery query) + /// + /// 测试桩:同步发送 CQRS 查询并返回结果。 + /// + /// 查询结果类型。 + /// 要发送的查询。 + /// 查询结果。 + /// 该测试桩未实现此成员。 + public TResponse SendQuery(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery query) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发布 CQRS 通知。 + /// + /// 通知类型。 + /// 要发布的通知。 + /// 取消令牌。 + /// 通知发布任务。 + /// 该测试桩未实现此成员。 public ValueTask PublishAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification { throw new NotImplementedException(); } - public IAsyncEnumerable CreateStream(IStreamRequest request, + /// + /// 测试桩:创建 CQRS 流式请求响应序列。 + /// + /// 流式响应元素类型。 + /// 流式请求。 + /// 取消令牌。 + /// 异步响应流。 + /// 该测试桩未实现此成员。 + public IAsyncEnumerable CreateStream( + IStreamRequest request, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送无返回值 CQRS 命令。 + /// + /// 命令类型。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令发送任务。 + /// 该测试桩未实现此成员。 public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : IRequest { throw new NotImplementedException(); } + /// + /// 测试桩:异步发送带返回值的 CQRS 请求。 + /// + /// 响应类型。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求响应任务。 + /// 该测试桩未实现此成员。 public ValueTask SendAsync(IRequest command, CancellationToken cancellationToken = default) { @@ -465,7 +545,7 @@ public class TestArchitectureContext : IArchitectureContext /// 返回值类型 /// 命令对象 /// 命令执行结果 - public TResult SendCommand(Abstractions.Command.ICommand command) + public TResult SendCommand(ICommand command) { return default!; } @@ -486,7 +566,7 @@ public class TestArchitectureContext : IArchitectureContext /// 查询结果类型 /// 查询对象 /// 查询结果 - public TResult SendQuery(Abstractions.Query.IQuery query) + public TResult SendQuery(IQuery query) { return default!; } @@ -510,4 +590,4 @@ public class TestArchitectureContext : IArchitectureContext { return Environment; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs b/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs index 24fd2681..0499a4ab 100644 --- a/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs +++ b/GFramework.Core.Tests/Architectures/PriorityServiceTests.cs @@ -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 \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs index 4e517194..b224804d 100644 --- a/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs +++ b/GFramework.Core.Tests/Architectures/RegistryInitializationHookBaseTests.cs @@ -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() where TBehavior : class + public void RegisterCqrsPipelineBehavior() where TBehavior : class { throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + throw new NotImplementedException(); + } + + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 要接入的程序集集合。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + throw new NotImplementedException(); + } + + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); + } + public IArchitectureModule InstallModule(IArchitectureModule module) { throw new NotImplementedException(); @@ -306,11 +332,37 @@ public class TestArchitectureWithoutRegistry : IArchitecture throw new NotImplementedException(); } - public void RegisterMediatorBehavior() where TBehavior : class + public void RegisterCqrsPipelineBehavior() where TBehavior : class { throw new NotImplementedException(); } + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + throw new NotImplementedException(); + } + + /// + /// 测试替身未实现显式程序集 CQRS 处理器接入入口。 + /// + /// 要接入的程序集集合。 + /// 该测试替身不参与 CQRS 程序集接入路径验证。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + throw new NotImplementedException(); + } + + [Obsolete("Use RegisterCqrsPipelineBehavior() instead.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); + } + public IArchitectureModule InstallModule(IArchitectureModule module) { throw new NotImplementedException(); @@ -363,4 +415,4 @@ public class TestArchitectureWithoutRegistry : IArchitecture public void RegisterLifecycleHook(IArchitectureLifecycleHook hook) { } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs b/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs index e7a0611e..2cc2ec13 100644 --- a/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs +++ b/GFramework.Core.Tests/Command/AbstractAsyncCommandTests.cs @@ -1,5 +1,4 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Architectures; using GFramework.Core.Command; @@ -7,6 +6,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Tests.Command; @@ -396,4 +396,4 @@ public sealed class TestAsyncCommandWithResultChildV3 : AbstractAsyncCommand -/// MediatorCoroutineExtensions的单元测试类 -/// 测试Mediator模式与协程集成的扩展方法 -/// 注意:由于 Mediator 使用源生成器,本测试类主要验证接口和参数验证 -/// -[TestFixture] -public class MediatorCoroutineExtensionsTests -{ - /// - /// 测试用的简单命令类 - /// - private class TestCommand : IRequest - { - public string Data { get; set; } = string.Empty; - } - - /// - /// 测试用的简单事件类 - /// - private class TestEvent - { - public string Data { get; set; } = string.Empty; - } - - /// - /// 上下文感知基类的模拟实现 - /// - private class TestContextAware : IContextAware - { - public readonly Mock _mockContext = new(); - - public IArchitectureContext GetContext() - { - return _mockContext.Object; - } - - public void SetContext(IArchitectureContext context) - { - } - } - - /// - /// 验证SendCommandCoroutine应该返回IEnumerator - /// - [Test] - public void SendCommandCoroutine_Should_Return_IEnumerator_Of_YieldInstruction() - { - var command = new TestCommand { Data = "Test" }; - var contextAware = new TestContextAware(); - - // 创建 mediator 模拟 - var mediatorMock = new Mock(); - contextAware._mockContext - .Setup(ctx => ctx.GetService()) - .Returns(mediatorMock.Object); - - var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command); - - Assert.That(coroutine, Is.InstanceOf>()); - } - - /// - /// 验证SendCommandCoroutine应该在mediator为null时抛出NullReferenceException - /// - [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()) - .Returns((IMediator?)null); - - // 创建协程 - var coroutine = MediatorCoroutineExtensions.SendCommandCoroutine(contextAware, command); - - // 调用 MoveNext 时应该抛出 NullReferenceException - Assert.Throws(() => coroutine.MoveNext()); - } -} \ No newline at end of file diff --git a/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs new file mode 100644 index 00000000..9406ede7 --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs @@ -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; + +/// +/// 为容器层测试提供可扫描的最小通知夹具。 +/// +internal sealed record DeterministicOrderNotification : INotification; + +/// +/// 供容器注册测试验证程序集扫描结果的通知处理器。 +/// +internal sealed class DeterministicOrderNotificationHandler : INotificationHandler +{ + /// + /// 无副作用地消费通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs new file mode 100644 index 00000000..64f69f02 --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/CqrsPublicNamespaceCompatibilityTests.cs @@ -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; + +/// +/// 锁定 CQRS 基础消息类型在 runtime 拆分后的公开命名空间与程序集兼容性。 +/// +[TestFixture] +public sealed class CqrsPublicNamespaceCompatibilityTests +{ + /// + /// 验证基础消息类型继续暴露在历史公开 CQRS 命名空间(GFramework.Cqrs.*),同时由独立 runtime 程序集承载实现。 + /// + [Test] + public void Base_Message_Types_Should_Live_In_Cqrs_Namespaces_And_Runtime_Assembly() + { + Assert.Multiple(() => + { + AssertLegacyType(typeof(CommandBase), "GFramework.Cqrs.Command"); + AssertLegacyType(typeof(QueryBase), "GFramework.Cqrs.Query"); + AssertLegacyType(typeof(RequestBase), "GFramework.Cqrs.Request"); + AssertLegacyType(typeof(NotificationBase), "GFramework.Cqrs.Notification"); + }); + } + + /// + /// 验证旧的 GFramework.Core 程序集限定名仍可解析到迁移后的 runtime 实现类型。 + /// + [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; +} diff --git a/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs new file mode 100644 index 00000000..a6cc927f --- /dev/null +++ b/GFramework.Core.Tests/Cqrs/MediatorCompatibilityDeprecationTests.cs @@ -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; + +/// +/// 锁定历史 Mediator 兼容入口的正式弃用策略。 +/// 这些测试确保旧 API 不仅保留行为兼容,还会通过编译期提示和 IntelliSense 隐藏引导调用方迁移到新的 CQRS 命名。 +/// +[TestFixture] +public class MediatorCompatibilityDeprecationTests +{ + /// + /// 验证公开兼容方法仍可用,但已被显式标记为未来移除的旧别名。 + /// + [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)); + } + + /// + /// 验证历史扩展类型会把迁移目标写入弃用说明,并从 IntelliSense 主路径隐藏。 + /// + [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."); + } + + /// + /// 断言方法级兼容 API 具备统一的弃用元数据。 + /// + /// 声明该方法的类型。 + /// 方法名称。 + 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(); + var editorBrowsableAttribute = method.GetCustomAttribute(); + + Assert.Multiple(() => + { + Assert.That(obsoleteAttribute, Is.Not.Null); + Assert.That( + obsoleteAttribute!.Message, + Does.Contain("Use RegisterCqrsPipelineBehavior() 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)); + }); + } + + /// + /// 断言类型级兼容扩展具备统一的弃用元数据。 + /// + /// 兼容扩展类型。 + /// 期望的迁移提示。 + private static void AssertLegacyType(Type type, string expectedReplacementHint) + { + var obsoleteAttribute = type.GetCustomAttribute(); + var editorBrowsableAttribute = type.GetCustomAttribute(); + + 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)); + }); + } +} diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index 5e59b144..391b2924 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -10,11 +10,6 @@ 0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -23,6 +18,7 @@ + diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index 18957f6b..96b8bb1a 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -16,6 +16,7 @@ global using System.Collections.Generic; global using System.Linq; global using System.Threading; global using System.Threading.Tasks; +global using GFramework.Tests.Common; global using NUnit.Framework; global using NUnit.Compatibility; global using GFramework.Core.Systems; @@ -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; \ No newline at end of file +global using GFramework.Core.Abstractions.Property; +global using Microsoft.Extensions.DependencyInjection; +global using Moq; diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 0621bcfc..19c59dcb 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -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)); } + /// + /// 测试当 CQRS 基础设施已手动接线后,再调用处理器注册入口不会重复注册 runtime seam。 + /// + [Test] + public void RegisterHandlers_Should_Not_Duplicate_Cqrs_Infrastructure_When_It_Is_Already_Registered() + { + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.Get(), Is.SameAs(_container.Get())); + + CqrsTestRuntime.RegisterHandlers(_container); + + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.GetAll(), Has.Count.EqualTo(1)); + Assert.That(_container.Get(), Is.SameAs(_container.Get())); + } + /// /// 测试当没有实例时获取应返回 null 的功能 /// @@ -224,6 +249,46 @@ public class MicrosoftDiContainerTests Assert.That(results.Count, Is.EqualTo(0)); } + /// + /// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复, + /// 但会保留同一服务类型的重复显式注册。 + /// + [Test] + public void GetAll_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases() + { + var instance = new AliasAwareService(); + + _container.Register(instance); + _container.Register(instance); + _container.Register(instance); + + var results = _container.GetAll(); + + Assert.That(results, Has.Count.EqualTo(2)); + Assert.That(results[0], Is.SameAs(instance)); + Assert.That(results[1], Is.SameAs(instance)); + } + + /// + /// 测试非泛型 GetAll 在容器未冻结时与泛型重载保持相同的别名去重语义。 + /// + [Test] + public void + GetAll_Type_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases() + { + var instance = new AliasAwareService(); + + _container.Register(instance); + _container.Register(instance); + _container.Register(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)); + } + /// /// 测试获取排序后的所有实例的功能 /// @@ -306,6 +371,47 @@ public class MicrosoftDiContainerTests Assert.That(_container.Contains(), Is.False); } + /// + /// 测试清空容器后可以重新接入同一程序集中的 CQRS 处理器。 + /// + [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)), + Is.True); + + _container.Clear(); + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.False); + + // Clear 会移除测试手工补齐的 CQRS seam,需要先恢复基础设施再验证程序集去重状态是否已重置。 + CqrsTestRuntime.RegisterInfrastructure(_container); + _container.RegisterCqrsHandlersFromAssembly(assembly); + + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); + } + + /// + /// 测试当程序集集合中包含空元素时,CQRS handler 注册入口会在委托给注册服务前直接失败。 + /// + [Test] + public void RegisterCqrsHandlersFromAssemblies_WithNullAssemblyItem_Should_ThrowArgumentNullException() + { + var assemblies = new Assembly[] { typeof(DeterministicOrderNotification).Assembly, null! }; + + Assert.Throws(() => _container.RegisterCqrsHandlersFromAssemblies(assemblies)); + } + /// /// 测试冻结容器以防止进一步注册的功能 /// @@ -661,6 +767,28 @@ public interface IMixedService string? Name { get; set; } } +/// +/// 用于验证未冻结查询路径中的服务别名去重行为。 +/// +public interface ISharedAliasService; + +/// +/// 主服务别名接口。 +/// +public interface IPrimaryAliasService : ISharedAliasService; + +/// +/// 次级兼容别名接口。 +/// +public interface ISecondaryAliasService : ISharedAliasService; + +/// +/// 同时实现多个别名接口的测试服务。 +/// +public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService +{ +} + /// /// 实现优先级的服务 /// @@ -676,4 +804,4 @@ public sealed class PrioritizedService : IPrioritizedService, IMixedService public sealed class NonPrioritizedService : IMixedService { public string? Name { get; set; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs b/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs index 8ba7ac91..d175b32f 100644 --- a/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs +++ b/GFramework.Core.Tests/Logging/LoggerFactoryTests.cs @@ -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; /// -/// 测试LoggerFactory相关功能的测试类 +/// 测试 LoggerFactory 相关功能的测试类。 /// [TestFixture] +[NonParallelizable] public class LoggerFactoryTests { /// - /// 测试ConsoleLoggerFactory的GetLogger方法是否返回ConsoleLogger实例 + /// 测试 ConsoleLoggerFactory 的 GetLogger 方法是否返回 ConsoleLogger 实例。 /// [Test] public void ConsoleLoggerFactory_GetLogger_ShouldReturnConsoleLogger() @@ -26,7 +27,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory使用不同名称获取不同的logger实例 + /// 测试 ConsoleLoggerFactory 使用不同名称获取不同的 logger 实例。 /// [Test] public void ConsoleLoggerFactory_GetLogger_WithDifferentNames_ShouldReturnDifferentLoggers() @@ -40,7 +41,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactory使用默认最小级别时的行为(默认为Info级别) + /// 测试 ConsoleLoggerFactory 使用默认最小级别时的行为。 /// [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 } /// - /// 测试ConsoleLoggerFactoryProvider创建logger时使用提供者的最小级别设置 + /// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供者的最小级别设置。 /// [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 } /// - /// 测试ConsoleLoggerFactoryProvider创建logger时使用提供的名称 + /// 测试 ConsoleLoggerFactoryProvider 创建 logger 时使用提供的名称。 /// [Test] public void ConsoleLoggerFactoryProvider_CreateLogger_ShouldUseProvidedName() @@ -94,7 +93,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider属性是否有默认值 + /// 测试 LoggerFactoryResolver 的 Provider 属性是否有默认值。 /// [Test] public void LoggerFactoryResolver_Provider_ShouldHaveDefaultValue() @@ -104,7 +103,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider属性可以被更改 + /// 测试 LoggerFactoryResolver 的 Provider 属性可以被更改。 /// [Test] public void LoggerFactoryResolver_Provider_CanBeChanged() @@ -120,7 +119,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性是否有默认值 + /// 测试 LoggerFactoryResolver 的 MinLevel 属性是否有默认值。 /// [Test] public void LoggerFactoryResolver_MinLevel_ShouldHaveDefaultValue() @@ -129,7 +128,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的MinLevel属性可以被更改 + /// 测试 LoggerFactoryResolver 的 MinLevel 属性可以被更改。 /// [Test] public void LoggerFactoryResolver_MinLevel_CanBeChanged() @@ -144,7 +143,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel属性是否有默认值 + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 属性是否有默认值。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_ShouldHaveDefaultValue() @@ -155,7 +154,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel属性可以被更改 + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 属性可以被更改。 /// [Test] public void ConsoleLoggerFactoryProvider_MinLevel_CanBeChanged() @@ -168,7 +167,7 @@ public class LoggerFactoryTests } /// - /// 测试LoggerFactoryResolver的Provider创建logger时使用提供者设置 + /// 测试 LoggerFactoryResolver 的 Provider 创建 logger 时使用提供者设置。 /// [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 } /// - /// 测试LoggerFactoryResolver的MinLevel属性影响新创建的logger + /// 测试 LoggerFactoryResolver 的 MinLevel 属性影响新创建的 logger。 /// [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 } /// - /// 测试ConsoleLoggerFactory创建的多个logger实例是独立的 + /// 验证默认 provider 激活失败时会回退到静默 provider。 + /// + [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; + } + } + + /// + /// 验证并发首次访问默认 provider 时只会创建一个实例,并向所有调用方返回相同引用。 + /// + [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; + } + } + + /// + /// 测试 ConsoleLoggerFactory 创建的多个 logger 实例是独立的。 /// [Test] public void ConsoleLoggerFactory_MultipleLoggers_ShouldBeIndependent() @@ -236,7 +319,7 @@ public class LoggerFactoryTests } /// - /// 测试ConsoleLoggerFactoryProvider的MinLevel不会影响已创建的logger + /// 测试 ConsoleLoggerFactoryProvider 的 MinLevel 不会影响已创建的 logger。 /// [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")); } -} \ No newline at end of file + + 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}."); + } + + /// + /// 用于触发默认 provider 激活失败回退路径的测试桩。 + /// + public sealed class ThrowingLoggerFactoryProvider : ILoggerFactoryProvider + { + /// + /// 初始化一个始终抛出异常的 provider。 + /// + /// 始终抛出,用于覆盖回退路径。 + public ThrowingLoggerFactoryProvider() + { + throw new InvalidOperationException("Simulated provider activation failure."); + } + + /// + /// 获取或设置最小日志级别。 + /// + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + /// + /// 创建日志器。 + /// + /// 日志器名称。 + /// 该测试桩永远不会成功创建日志器。 + /// 始终抛出,因为该方法不应被调用。 + public ILogger CreateLogger(string name) + { + throw new NotSupportedException(); + } + } + + /// + /// 用于验证并发首次初始化路径只创建单个 provider 实例的测试桩。 + /// + public sealed class BlockingLoggerFactoryProvider : ILoggerFactoryProvider + { + private static int _constructionCount; + private static ManualResetEventSlim _constructionGate = new(false); + + /// + /// 初始化一个会阻塞构造完成的 provider,用于放大并发首次访问竞争窗口。 + /// + public BlockingLoggerFactoryProvider() + { + Interlocked.Increment(ref _constructionCount); + _constructionGate.Wait(TimeSpan.FromSeconds(5)); + } + + /// + /// 获取已经发生的构造次数。 + /// + public static int ConstructionCount => Volatile.Read(ref _constructionCount); + + /// + /// 获取或设置最小日志级别。 + /// + public LogLevel MinLevel { get; set; } = LogLevel.Info; + + /// + /// 创建测试日志器。 + /// + /// 日志器名称。 + /// 带有当前最小级别设置的控制台日志器。 + public ILogger CreateLogger(string name) + { + return new ConsoleLogger(name, MinLevel, TextWriter.Null, false); + } + + /// + /// 重置该测试桩的并发观测状态。 + /// + public static void Reset() + { + _constructionGate = new ManualResetEventSlim(false); + Interlocked.Exchange(ref _constructionCount, 0); + } + + /// + /// 释放当前被阻塞的 provider 构造过程。 + /// + public static void ReleaseConstruction() + { + _constructionGate.Set(); + } + } +} diff --git a/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs b/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs index 7a645ac1..bf6397b9 100644 --- a/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs +++ b/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs @@ -1,4 +1,3 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Architectures; @@ -7,6 +6,7 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -411,4 +411,4 @@ public sealed class TestAsyncQueryResultV2 /// 获取或设置双倍值 /// public int DoubleValue { get; init; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs index 6491946d..b23cc8f9 100644 --- a/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs @@ -1,5 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -292,4 +292,4 @@ public sealed class TestAsyncQueryResult /// 获取或设置双倍值 /// public int DoubleValue { get; init; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Query/QueryExecutorTests.cs b/GFramework.Core.Tests/Query/QueryExecutorTests.cs index f0305b4b..a9cde117 100644 --- a/GFramework.Core.Tests/Query/QueryExecutorTests.cs +++ b/GFramework.Core.Tests/Query/QueryExecutorTests.cs @@ -1,5 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -121,4 +121,4 @@ public sealed class TestStringQuery : AbstractQuery { return $"Result: {input.Value * 2}"; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/State/StateMachineSystemTests.cs b/GFramework.Core.Tests/State/StateMachineSystemTests.cs index 0ea62de6..2c234238 100644 --- a/GFramework.Core.Tests/State/StateMachineSystemTests.cs +++ b/GFramework.Core.Tests/State/StateMachineSystemTests.cs @@ -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 \ No newline at end of file +#endregion diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 39b4fd13..8d2582aa 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -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 /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 /// 可以传入开放泛型行为类型,也可以传入绑定到特定请求的封闭行为类型。 /// /// 行为类型,必须是引用类型 + public void RegisterCqrsPipelineBehavior() where TBehavior : class + { + _modules.RegisterCqrsPipelineBehavior(); + } + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 + /// + /// 行为类型,必须是引用类型 + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { - _modules.RegisterMediatorBehavior(); + RegisterCqrsPipelineBehavior(); + } + + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + _modules.RegisterCqrsHandlersFromAssembly(assembly); + } + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。 + /// + /// 要接入的程序集集合。 + /// + /// 当前架构的底层容器已冻结,无法继续注册处理器。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + _modules.RegisterCqrsHandlersFromAssemblies(assemblies); } /// @@ -328,4 +366,4 @@ public abstract class Architecture : IArchitecture } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs index 984c0a07..343bcdd6 100644 --- a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs +++ b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs @@ -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( /// 因为用户初始化逻辑通常会立即访问事件总线、查询执行器或环境对象。 /// /// 调用方已经提供的上下文;如果为空则创建默认上下文。 - /// 可选的容器配置委托,用于接入 Mediator 等扩展服务。 + /// 可选的容器配置委托,用于接入额外服务或覆盖默认依赖绑定。 /// 是否以异步模式初始化服务模块。 /// 已绑定到当前架构类型的架构上下文。 public async Task PrepareForInitializationAsync( @@ -92,16 +91,21 @@ internal sealed class ArchitectureBootstrapper( /// /// 为服务容器设置上下文并执行扩展配置钩子。 - /// 这一步统一承接 Mediator 等容器扩展的接入点,避免 直接操作容器细节。 + /// 这一步统一承接 CQRS 运行时与容器扩展的接入点,避免 直接操作容器细节。 /// /// 当前架构上下文。 /// 可选的服务集合配置委托。 private void ConfigureServices(IArchitectureContext context, Action? 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); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 1e3d72a7..e0ac2dd6 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -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; /// /// 架构上下文类,提供对系统、模型、工具等组件的访问以及命令、查询、事件的执行管理 /// -public class ArchitectureContext(IIocContainer container) : IArchitectureContext +public class ArchitectureContext : IArchitectureContext { - private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly IIocContainer _container; + private readonly Lazy _cqrsRuntime; private readonly ConcurrentDictionary _serviceCache = new(); - #region Mediator Integration + /// + /// 初始化新的架构上下文,并绑定其依赖容器。 + /// + /// + /// 当前架构使用的 IOC 容器。 + /// CQRS runtime 与其他框架服务会通过该容器延迟解析,以避免在上下文构造阶段强制拉起整条运行时链路。 + /// + /// + public ArchitectureContext(IIocContainer container) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _cqrsRuntime = new Lazy( + ResolveCqrsRuntime, + LazyThreadSafetyMode.ExecutionAndPublication); + } + + #region CQRS Integration /// - /// 获取 Mediator 实例(延迟加载) + /// 获取 CQRS runtime seam。 /// - private IMediator Mediator => GetOrCache(); + /// + /// 该实例会在首次访问时从容器解析,并通过 保证并发场景下只执行一次初始化, + /// 避免多个请求线程重复触发同一个 runtime 的容器解析。 + /// + private ICqrsRuntime CqrsRuntime => _cqrsRuntime.Value; /// - /// 获取 ISender 实例(更轻量的发送器) + /// 从容器解析当前架构上下文依赖的 CQRS runtime。 /// - private ISender Sender => GetOrCache(); - - /// - /// 获取 IPublisher 实例(用于发布通知) - /// - private IPublisher Publisher => GetOrCache(); + /// 已注册的 CQRS runtime 实例。 + /// 容器中未注册 + private ICqrsRuntime ResolveCqrsRuntime() + { + return _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); + } /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 @@ -64,30 +85,23 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发送请求(Command/Query) - /// 这是推荐的新方式,统一处理命令和查询 + /// 发送请求(Command/Query) + /// 使用 GFramework 自有 CQRS runtime 统一处理命令和查询。 /// /// 响应类型 /// 请求对象(Command 或 Query) /// 取消令牌 /// 响应结果 - /// 当 Mediator 未注册时抛出 public async ValueTask SendRequestAsync( IRequest 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); } /// - /// [Mediator] 发送请求的同步版本(不推荐,仅用于兼容性) + /// 发送请求的同步版本(不推荐,仅用于兼容性) /// /// 响应类型 /// 请求对象 @@ -98,8 +112,8 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发布通知(一对多) - /// 用于事件驱动场景,多个处理器可以同时处理同一个通知 + /// 发布通知(一对多) + /// 使用 GFramework 自有 CQRS runtime 分发到所有已注册通知处理器。 /// /// 通知类型 /// 通知对象 @@ -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); } /// - /// [Mediator] 发送请求并返回流(用于大数据集) + /// 发送请求并返回流(用于大数据集) /// /// 响应项类型 /// 流式请求 @@ -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); } /// @@ -171,7 +175,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询结果类型 /// 要发送的查询 /// 查询结果 - public TResult SendQuery(Abstractions.Query.IQuery query) + public TResult SendQuery(IQuery query) { if (query == null) throw new ArgumentNullException(nameof(query)); var queryBus = GetOrCache(); @@ -180,12 +184,12 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// 发送 CQRS 查询的同步版本(不推荐,仅用于兼容性) /// /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(Mediator.IQuery query) + public TResponse SendQuery(Cqrs.Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -205,23 +209,17 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 异步发送查询并返回结果 - /// 通过Mediator模式发送查询请求,支持取消操作 + /// 异步发送 CQRS 查询并返回结果。 /// /// 查询响应类型 /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(Mediator.IQuery query, + public async ValueTask SendQueryAsync(Cqrs.Abstractions.Cqrs.Query.IQuery 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 /// - /// [Mediator] 异步发送命令并返回结果 - /// 通过Mediator模式发送命令请求,支持取消操作 + /// 异步发送 CQRS 命令并返回结果。 /// /// 命令响应类型 /// 要发送的命令对象 /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask - public async ValueTask SendCommandAsync(Mediator.ICommand command, + public async ValueTask SendCommandAsync( + Cqrs.Abstractions.Cqrs.Command.ICommand 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); } /// @@ -393,12 +386,12 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } /// - /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// 发送 CQRS 命令的同步版本(不推荐,仅用于兼容性) /// /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(Mediator.ICommand command) + public TResponse SendCommand(Cqrs.Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } @@ -420,7 +413,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令执行结果类型 /// 要发送的命令 /// 命令执行结果 - public TResult SendCommand(Abstractions.Command.ICommand command) + public TResult SendCommand(ICommand command) { ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); @@ -491,4 +484,4 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureModules.cs b/GFramework.Core/Architectures/ArchitectureModules.cs index 94acae78..f5d2a55d 100644 --- a/GFramework.Core/Architectures/ArchitectureModules.cs +++ b/GFramework.Core/Architectures/ArchitectureModules.cs @@ -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; /// /// 架构模块管理器 -/// 负责管理架构模块的安装和中介行为注册 +/// 负责管理架构模块的安装和 CQRS 行为注册 /// internal sealed class ArchitectureModules( IArchitecture architecture, @@ -13,15 +15,56 @@ internal sealed class ArchitectureModules( ILogger logger) { /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 /// 支持开放泛型行为类型和针对单一请求的封闭行为类型。 /// /// 行为类型,必须是引用类型 + public void RegisterCqrsPipelineBehavior() where TBehavior : class + { + logger.Debug($"Registering CQRS pipeline behavior: {typeof(TBehavior).Name}"); + services.Container.RegisterCqrsPipelineBehavior(); + } + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 + /// + /// 行为类型,必须是引用类型 + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] public void RegisterMediatorBehavior() where TBehavior : class { - logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}"); - services.Container.RegisterMediatorBehavior(); + RegisterCqrsPipelineBehavior(); + } + + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 底层容器已冻结,无法继续注册处理器。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + ArgumentNullException.ThrowIfNull(assembly); + logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}"); + services.Container.RegisterCqrsHandlersFromAssembly(assembly); + } + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。 + /// + /// 要接入的程序集集合。 + /// + /// 底层容器已冻结,无法继续注册处理器。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + logger.Debug("Registering CQRS handlers from additional assemblies."); + services.Container.RegisterCqrsHandlersFromAssemblies(assemblies); } /// @@ -37,4 +80,4 @@ internal sealed class ArchitectureModules( logger.Info($"Module installed: {name}"); return module; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs index 491bbd8e..d97ab90b 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -26,4 +26,4 @@ public abstract class AbstractAsyncCommand(TInput input) : ContextAwareB /// 命令输入参数 /// 表示异步操作的任务 protected abstract Task OnExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs index 47390192..ae19458e 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs @@ -1,6 +1,6 @@ using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -27,4 +27,4 @@ public abstract class AbstractAsyncCommand(TInput input) : Cont /// 命令输入参数 /// 表示异步操作且包含结果的任务 protected abstract Task OnExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractCommandWithInput.cs b/GFramework.Core/Command/AbstractCommandWithInput.cs index 5326fe6e..c3ebbb03 100644 --- a/GFramework.Core/Command/AbstractCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractCommandWithInput.cs @@ -1,6 +1,6 @@ -using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Command; @@ -25,4 +25,4 @@ public abstract class AbstractCommand(TInput input) : ContextAwareBase, /// /// 命令执行所需的输入参数 protected abstract void OnExecute(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractCommandWithResult.cs b/GFramework.Core/Command/AbstractCommandWithResult.cs index 67901821..ca40957a 100644 --- a/GFramework.Core/Command/AbstractCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractCommandWithResult.cs @@ -1,6 +1,5 @@ -using GFramework.Core.Abstractions.Command; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -10,14 +9,15 @@ namespace GFramework.Core.Command; /// 命令输入参数类型,必须实现 ICommandInput 接口 /// 命令执行后返回的结果类型 /// 命令执行所需的输入参数 -public abstract class AbstractCommand(TInput input) : ContextAwareBase, ICommand +public abstract class AbstractCommand(TInput input) + : ContextAwareBase, Abstractions.Command.ICommand where TInput : ICommandInput { /// /// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法 /// /// 命令执行后的结果 - TResult ICommand.Execute() + TResult Abstractions.Command.ICommand.Execute() { return OnExecute(input); } @@ -28,4 +28,4 @@ public abstract class AbstractCommand(TInput input) : ContextAw /// 命令执行所需的输入参数 /// 命令执行后的结果 protected abstract TResult OnExecute(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/EmptyCommandInput.cs b/GFramework.Core/Command/EmptyCommandInput.cs index 4d08f4e7..540c158e 100644 --- a/GFramework.Core/Command/EmptyCommandInput.cs +++ b/GFramework.Core/Command/EmptyCommandInput.cs @@ -1,4 +1,4 @@ -using GFramework.Core.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; namespace GFramework.Core.Command; @@ -9,4 +9,4 @@ namespace GFramework.Core.Command; /// 该类实现了ICommandInput接口,作为命令模式中的输入参数载体 /// 通常用于不需要额外输入参数的简单命令操作 /// -public sealed class EmptyCommandInput : ICommandInput; \ No newline at end of file +public sealed class EmptyCommandInput : ICommandInput; diff --git a/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs new file mode 100644 index 00000000..ec2b55a0 --- /dev/null +++ b/GFramework.Core/Coroutine/Extensions/CqrsCoroutineExtensions.cs @@ -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; + +/// +/// 提供 CQRS 命令与协程集成的扩展方法。 +/// 这些扩展直接走架构上下文的内建 CQRS runtime,不依赖外部 Mediator 服务。 +/// +public static class CqrsCoroutineExtensions +{ + /// + /// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。 + /// + /// 命令类型。 + /// 上下文感知对象,用于获取架构上下文。 + /// 要发送的命令对象。 + /// 发生异常时的回调处理函数。 + /// 协程枚举器,用于协程执行。 + /// + /// 当 时抛出。 + /// + /// + /// 当底层命令调度被取消且未提供 时抛出。 + /// + /// + /// 当底层命令调度失败且未提供 时,抛出底层原始异常。 + /// + /// + /// 当底层命令调度失败时,该扩展会把底层异常解包后传给 , + /// 在取消时则统一暴露 ,避免成功、失败与取消三种完成状态被混淆。 + /// + public static IEnumerator SendCommandCoroutine( + this IContextAware contextAware, + TCommand command, + Action? onError = null) + where TCommand : IRequest + { + 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(); + } +} diff --git a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs index 5c301c07..5086e392 100644 --- a/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs +++ b/GFramework.Core/Coroutine/Extensions/MediatorCoroutineExtensions.cs @@ -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; /// -/// 提供Mediator模式与协程集成的扩展方法。 -/// 包含发送命令和等待事件的协程实现。 +/// 提供 CQRS 命令与协程集成的扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// +[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 { /// - /// 以协程方式发送命令并处理可能的异常。 + /// 以协程方式发送无返回值 CQRS 命令并处理可能的异常。 /// - /// 命令的类型 + /// 命令的类型。 /// 上下文感知对象,用于获取服务 /// 要发送的命令对象 /// 发生异常时的回调处理函数 @@ -35,20 +40,8 @@ public static class MediatorCoroutineExtensions this IContextAware contextAware, TCommand command, Action? onError = null) - where TCommand : notnull + where TCommand : IRequest { - var mediator = contextAware - .GetContext() - .GetService()!; - - 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); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs deleted file mode 100644 index 064cb279..00000000 --- a/GFramework.Core/Cqrs/Command/AbstractStreamCommandHandler.cs +++ /dev/null @@ -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; - -/// -/// 抽象流式命令处理器基类 -/// 继承自ContextAwareBase并实现IStreamCommandHandler接口,为具体的流式命令处理器提供基础功能 -/// 支持流式处理命令并产生异步可枚举的响应序列 -/// -/// 流式命令类型,必须实现IStreamCommand接口 -/// 流式命令响应元素类型 -public abstract class AbstractStreamCommandHandler : ContextAwareBase, - IStreamCommandHandler - where TCommand : IStreamCommand -{ - /// - /// 处理流式命令并返回异步可枚举的响应序列 - /// 由具体的流式命令处理器子类实现流式处理逻辑 - /// - /// 要处理的流式命令对象 - /// 取消令牌,用于取消流式处理操作 - /// 异步可枚举的响应序列,每个元素类型为TResponse - public abstract IAsyncEnumerable Handle(TCommand command, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/GFramework.Core/Cqrs/Notification/NotificationBase.cs b/GFramework.Core/Cqrs/Notification/NotificationBase.cs deleted file mode 100644 index 96e26ea1..00000000 --- a/GFramework.Core/Cqrs/Notification/NotificationBase.cs +++ /dev/null @@ -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; - -/// -/// 表示一个基础通知类,用于处理带有输入的通知模式实现。 -/// 该类实现了 INotification 接口,提供了通用的通知结构。 -/// -/// 通知输入数据的类型,必须实现 INotificationInput 接口 -/// 通知执行所需的输入数据 -public abstract class NotificationBase(TInput input) : INotification where TInput : INotificationInput -{ - /// - /// 获取通知的输入数据。 - /// - public TInput Input => input; -} \ No newline at end of file diff --git a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs deleted file mode 100644 index 50cf1817..00000000 --- a/GFramework.Core/Cqrs/Query/AbstractStreamQueryHandler.cs +++ /dev/null @@ -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; - -/// -/// 抽象流式查询处理器基类 -/// 继承自ContextAwareBase并实现IStreamQueryHandler接口,为具体的流式查询处理器提供基础功能 -/// 支持流式处理查询并产生异步可枚举的响应序列,适用于大数据量或实时数据查询场景 -/// -/// 流式查询类型,必须实现IStreamQuery接口 -/// 流式查询响应元素类型 -public abstract class AbstractStreamQueryHandler : ContextAwareBase, - IStreamQueryHandler - where TQuery : IStreamQuery -{ - /// - /// 处理流式查询并返回异步可枚举的响应序列 - /// 由具体的流式查询处理器子类实现流式查询处理逻辑 - /// - /// 要处理的流式查询对象 - /// 取消令牌,用于取消流式查询操作 - /// 异步可枚举的响应序列,每个元素类型为TResponse - public abstract IAsyncEnumerable Handle(TQuery query, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/GFramework.Core/Cqrs/Query/QueryBase.cs b/GFramework.Core/Cqrs/Query/QueryBase.cs deleted file mode 100644 index 2ef0f34b..00000000 --- a/GFramework.Core/Cqrs/Query/QueryBase.cs +++ /dev/null @@ -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; - -/// -/// 表示一个基础查询类,用于处理带有输入和响应的查询模式实现。 -/// 该类继承自 Mediator.IQuery<TResponse> 接口,提供了通用的查询结构。 -/// -/// 查询输入数据的类型,必须实现 IQueryInput 接口 -/// 查询执行后返回结果的类型 -/// 查询执行所需的输入数据 -public abstract class QueryBase(TInput input) : IQuery where TInput : IQueryInput -{ - /// - /// 获取查询的输入数据。 - /// - public TInput Input => input; -} \ No newline at end of file diff --git a/GFramework.Core/Cqrs/Request/RequestBase.cs b/GFramework.Core/Cqrs/Request/RequestBase.cs deleted file mode 100644 index 8b878750..00000000 --- a/GFramework.Core/Cqrs/Request/RequestBase.cs +++ /dev/null @@ -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; - -/// -/// 表示一个基础请求类,用于处理带有输入和响应的请求模式实现。 -/// 该类实现了 IRequest<TResponse> 接口,提供了通用的请求结构。 -/// -/// 请求输入数据的类型,必须实现 IRequestInput 接口 -/// 请求执行后返回结果的类型 -/// 请求执行所需的输入数据 -public abstract class RequestBase(TInput input) : IRequest where TInput : IRequestInput -{ - /// - /// 获取请求的输入数据。 - /// - public TInput Input => input; -} \ No newline at end of file diff --git a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs index 6daccd26..24490239 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorCommandExtensions.cs @@ -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; /// -/// 提供对 IContextAware 接口的 Mediator 命令扩展方法 -/// 使用 Mediator 库的命令模式 +/// 提供对 接口的 CQRS 命令扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// +[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 { /// - /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// 发送命令的同步版本(不推荐,仅用于兼容性) /// /// 命令响应类型 /// 实现 IContextAware 接口的对象 @@ -20,15 +26,11 @@ public static class ContextAwareMediatorCommandExtensions public static TResponse SendCommand(this IContextAware contextAware, ICommand command) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var context = contextAware.GetContext(); - return context.SendCommand(command); + return ContextAwareCqrsCommandExtensions.SendCommand(contextAware, command); } /// - /// [Mediator] 异步发送命令并返回结果 + /// 异步发送命令并返回结果 /// /// 命令响应类型 /// 实现 IContextAware 接口的对象 @@ -39,10 +41,9 @@ public static class ContextAwareMediatorCommandExtensions public static ValueTask SendCommandAsync(this IContextAware contextAware, ICommand 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); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs index fa0d699d..c7aec1b6 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorExtensions.cs @@ -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; /// -/// 提供对 IContextAware 接口的 Mediator 统一接口扩展方法 +/// 提供对 接口的 CQRS 统一接口扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// +[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 { /// @@ -20,11 +27,10 @@ public static class ContextAwareMediatorExtensions public static ValueTask SendRequestAsync(this IContextAware contextAware, IRequest 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); } /// @@ -38,11 +44,7 @@ public static class ContextAwareMediatorExtensions public static TResponse SendRequest(this IContextAware contextAware, IRequest request) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(request); - - var context = contextAware.GetContext(); - return context.SendRequest(request); + return ContextAwareCqrsExtensions.SendRequest(contextAware, request); } /// @@ -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); } /// @@ -77,11 +78,10 @@ public static class ContextAwareMediatorExtensions public static IAsyncEnumerable CreateStream(this IContextAware contextAware, IStreamRequest 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); } /// @@ -97,11 +97,10 @@ public static class ContextAwareMediatorExtensions CancellationToken cancellationToken = default) where TCommand : IRequest { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(command); - - var context = contextAware.GetContext(); - return context.SendAsync(command, cancellationToken); + return ContextAwareCqrsExtensions.SendAsync( + contextAware, + command, + cancellationToken); } /// @@ -116,10 +115,9 @@ public static class ContextAwareMediatorExtensions public static ValueTask SendAsync(this IContextAware contextAware, IRequest 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); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs index cbdb01b4..cf0b4513 100644 --- a/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareMediatorQueryExtensions.cs @@ -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; /// -/// 提供对 IContextAware 接口的 Mediator 查询扩展方法 -/// 使用 Mediator 库的查询模式 +/// 提供对 接口的 CQRS 查询扩展方法。 +/// 该类型保留旧名称以兼容历史调用点;新代码应改用 。 +/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。 /// +[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 { /// - /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// 发送查询的同步版本(不推荐,仅用于兼容性) /// /// 查询响应类型 /// 实现 IContextAware 接口的对象 @@ -19,15 +25,11 @@ public static class ContextAwareMediatorQueryExtensions /// 当 contextAware 或 query 为 null 时抛出 public static TResponse SendQuery(this IContextAware contextAware, IQuery query) { - ArgumentNullException.ThrowIfNull(contextAware); - ArgumentNullException.ThrowIfNull(query); - - var context = contextAware.GetContext(); - return context.SendQuery(query); + return ContextAwareCqrsQueryExtensions.SendQuery(contextAware, query); } /// - /// [Mediator] 异步发送查询并返回结果 + /// 异步发送查询并返回结果 /// /// 查询响应类型 /// 实现 IContextAware 接口的对象 @@ -38,10 +40,9 @@ public static class ContextAwareMediatorQueryExtensions public static ValueTask SendQueryAsync(this IContextAware contextAware, IQuery 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); } -} \ No newline at end of file +} diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index c450b44c..2535d4e9 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -9,6 +9,8 @@ true + + diff --git a/GFramework.Core/GlobalUsings.cs b/GFramework.Core/GlobalUsings.cs index 8add267e..203366e6 100644 --- a/GFramework.Core/GlobalUsings.cs +++ b/GFramework.Core/GlobalUsings.cs @@ -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; \ No newline at end of file +global using System.Threading.Channels; +global using Microsoft.Extensions.DependencyInjection; diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 2615ca52..d1a0576d 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -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 + /// + /// 记录某个实例在未冻结查询中可见的服务类型分组信息。 + /// + /// 当前分组对应的服务类型。 + /// 该服务类型下的描述符数量。 + /// 该服务类型首次出现的位置,用于稳定打破并列。 + private sealed record VisibleServiceTypeGroup(Type ServiceType, int Count, int FirstIndex); + #region Fields /// @@ -310,13 +319,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// - /// 注册中介行为管道 - /// 用于配置Mediator框架的行为拦截和处理逻辑。 + /// 注册 CQRS 请求管道行为。 /// 同时支持开放泛型行为类型和已闭合的具体行为类型, /// 以兼容通用行为和针对单一请求的专用行为两种注册方式。 /// /// 行为类型,必须是引用类型 - public void RegisterMediatorBehavior() where TBehavior : class + public void RegisterCqrsPipelineBehavior() 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(); + } + } + + /// + /// 注册 CQRS 请求管道行为。 + /// 该成员保留旧名称以兼容历史调用点,内部行为与 一致。 + /// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。 + /// + /// 行为类型,必须是引用类型 + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Use RegisterCqrsPipelineBehavior() instead. This compatibility alias will be removed in a future major version.")] + public void RegisterMediatorBehavior() where TBehavior : class + { + RegisterCqrsPipelineBehavior(); + } + + /// + /// 从指定程序集显式注册 CQRS 处理器。 + /// + /// 包含 CQRS 处理器或生成注册器的程序集。 + /// + /// 容器已冻结,无法继续注册 CQRS 处理器。 + public void RegisterCqrsHandlersFromAssembly(Assembly assembly) + { + ArgumentNullException.ThrowIfNull(assembly); + RegisterCqrsHandlersFromAssemblies([assembly]); + } + + /// + /// 从多个程序集显式注册 CQRS 处理器。 + /// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。 + /// + /// 要接入的程序集集合。 + /// + /// 中存在 元素。 + /// 容器已冻结,无法继续注册 CQRS 处理器。 + public void RegisterCqrsHandlersFromAssemblies(IEnumerable 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 + /// + /// 获取当前容器中已注册的 CQRS 程序集注册协调器。 + /// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定, + /// 避免在容器未冻结前依赖完整的服务提供者构建流程。 + /// + /// 已注册的 CQRS 程序集注册协调器实例。 + /// 未找到可用的 CQRS 程序集注册协调器实例时抛出。 + 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); + } + /// /// 获取指定泛型类型的服务实例 /// 返回第一个匹配的注册实例,如果不存在则返回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(); - 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().ToList(); } var services = _provider!.GetServices().ToList(); @@ -563,37 +625,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// /// 服务类型 /// 只读的服务实例列表 - /// 当容器未冻结时抛出 + /// 时抛出 public IReadOnlyList 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(); - 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) } } + /// + /// 在容器未冻结时,从服务描述符中收集当前可直接观察到的实例绑定。 + /// + /// 调用方请求的服务类型。 + /// 按当前未冻结语义可见的实例列表。 + /// + /// 该方法只读取 ,因为未冻结路径不会主动执行工厂方法, + /// 也不会提前构造 。 + /// 若同一实例同时经由多个可赋值的 暴露, + /// 这里会把它视为兼容别名并只保留一个规范服务类型对应的结果; + /// 但同一 的重复显式注册仍会完整保留,以维持注册顺序和多次注册语义。 + /// + private List 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(); + 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; + } + + /// + /// 为每个可见实例选择一个规范服务类型,避免同一实例因兼容别名重复出现在未冻结查询结果中。 + /// + /// 已按请求类型过滤过的服务描述符集合。 + /// 调用方请求的服务类型。 + /// 实例到其规范服务类型的映射。 + private static Dictionary BuildPreferredVisibleServiceTypes( + IReadOnlyList matchingDescriptors, + Type requestedServiceType) + { + var preferredServiceTypes = new Dictionary(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; + } + + /// + /// 在“同一实例被多个服务类型暴露”的场景下,选择未冻结查询结果应保留的规范服务类型。 + /// + /// 引用同一实例的服务描述符。 + /// 调用方请求的服务类型。 + /// 应在结果中保留的服务类型。 + private static Type SelectPreferredVisibleServiceType( + IEnumerable 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; + } + /// /// 获取并排序指定泛型类型的所有服务实例 /// 主要用于系统调度场景 @@ -804,4 +948,4 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/LoggerFactoryResolver.cs b/GFramework.Core/Logging/LoggerFactoryResolver.cs deleted file mode 100644 index c15378d1..00000000 --- a/GFramework.Core/Logging/LoggerFactoryResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using GFramework.Core.Abstractions.Logging; - -namespace GFramework.Core.Logging; - -/// -/// 日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例 -/// -public static class LoggerFactoryResolver -{ - /// - /// 获取或设置当前的日志工厂提供程序 - /// - /// - /// 日志工厂提供程序实例,默认为控制台日志工厂提供程序 - /// - public static ILoggerFactoryProvider Provider { get; set; } - = new ConsoleLoggerFactoryProvider(); - - /// - /// 获取或设置日志记录的最小级别 - /// - /// - /// 日志级别枚举值,默认为Info级别 - /// - public static LogLevel MinLevel { get; set; } = LogLevel.Info; -} \ No newline at end of file diff --git a/GFramework.Core/Properties/TypeForwarders.cs b/GFramework.Core/Properties/TypeForwarders.cs new file mode 100644 index 00000000..2c260462 --- /dev/null +++ b/GFramework.Core/Properties/TypeForwarders.cs @@ -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<>))] diff --git a/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs b/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs index 03c35071..03713712 100644 --- a/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractAsyncQueryWithResult.cs @@ -1,6 +1,6 @@ -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Query; +using GFramework.Core.Abstractions.Query; using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -30,4 +30,4 @@ public abstract class AbstractAsyncQuery( /// 查询输入参数 /// 返回查询结果的异步任务 protected abstract Task OnDoAsync(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Query/AbstractQueryWithResult.cs b/GFramework.Core/Query/AbstractQueryWithResult.cs index cb46071a..ae099abe 100644 --- a/GFramework.Core/Query/AbstractQueryWithResult.cs +++ b/GFramework.Core/Query/AbstractQueryWithResult.cs @@ -1,6 +1,5 @@ -using GFramework.Core.Abstractions.Cqrs.Query; -using GFramework.Core.Abstractions.Query; -using GFramework.Core.Rule; +using GFramework.Core.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -9,7 +8,8 @@ namespace GFramework.Core.Query; /// /// 查询输入参数的类型,必须实现IQueryInput接口 /// 查询结果的类型 -public abstract class AbstractQuery(TInput input) : ContextAwareBase, IQuery +public abstract class AbstractQuery(TInput input) + : ContextAwareBase, Abstractions.Query.IQuery where TInput : IQueryInput { /// @@ -27,4 +27,4 @@ public abstract class AbstractQuery(TInput input) : ContextAwar /// 查询输入参数 /// 查询结果,类型为TResult protected abstract TResult OnDo(TInput input); -} \ No newline at end of file +} diff --git a/GFramework.Core/Query/EmptyQueryInput.cs b/GFramework.Core/Query/EmptyQueryInput.cs index 7d707189..a6a0cd67 100644 --- a/GFramework.Core/Query/EmptyQueryInput.cs +++ b/GFramework.Core/Query/EmptyQueryInput.cs @@ -1,4 +1,4 @@ -using GFramework.Core.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Query; @@ -8,4 +8,4 @@ namespace GFramework.Core.Query; /// /// 该类实现了IQueryInput接口,作为占位符使用,适用于那些不需要额外输入参数的查询场景 /// -public sealed class EmptyQueryInput : IQueryInput; \ No newline at end of file +public sealed class EmptyQueryInput : IQueryInput; diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs new file mode 100644 index 00000000..1da8f684 --- /dev/null +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -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; + +/// +/// CQRS runtime 模块,用于把默认请求分发器与处理器注册器接入架构容器。 +/// 该模块在架构初始化早期完成注册,保证用户初始化阶段即可使用 CQRS 入口与 handler 自动接入能力。 +/// +public sealed class CqrsRuntimeModule : IServiceModule +{ + /// + /// 获取模块名称。 + /// + public string ModuleName => nameof(CqrsRuntimeModule); + + /// + /// 获取模块优先级。 + /// CQRS runtime 需要先于架构默认 handler 扫描路径可用,因此放在基础总线模块之后、用户初始化之前注册。 + /// + public int Priority => 15; + + /// + /// 获取模块启用状态,默认启用。 + /// + public bool IsEnabled => true; + + /// + /// 注册默认 CQRS runtime seam 实现。 + /// + /// 目标依赖注入容器。 + public void Register(IIocContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("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)runtime); + container.Register(registrar); + container.Register( + CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger)); + } + + /// + /// 初始化模块。 + /// + public void Initialize() + { + } + + /// + /// 异步销毁模块。 + /// + /// 已完成的值任务。 + public ValueTask DestroyAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core/Services/ServiceModuleManager.cs b/GFramework.Core/Services/ServiceModuleManager.cs index d07c128b..a3965f4d 100644 --- a/GFramework.Core/Services/ServiceModuleManager.cs +++ b/GFramework.Core/Services/ServiceModuleManager.cs @@ -42,7 +42,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager /// /// 注册内置服务模块,并根据优先级排序后完成服务注册。 - /// 内置模块包括事件总线、命令执行器、查询执行器等核心模块。 + /// 内置模块包括事件总线、命令执行器、CQRS runtime、查询执行器等核心模块。 /// 同时注册通过 ArchitectureModuleRegistry 自动注册的外部模块。 /// /// IoC容器实例,用于模块服务注册。 @@ -57,6 +57,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager // 注册内置模块 RegisterModule(new EventBusModule()); RegisterModule(new CommandExecutorModule()); + RegisterModule(new CqrsRuntimeModule()); RegisterModule(new QueryExecutorModule()); RegisterModule(new AsyncQueryExecutorModule()); @@ -148,4 +149,4 @@ public sealed class ServiceModuleManager : IServiceModuleManager _builtInModulesRegistered = false; _logger.Info("All service modules destroyed"); } -} \ No newline at end of file +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs new file mode 100644 index 00000000..9c62f8f5 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs @@ -0,0 +1,13 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; + +/// +/// 表示一个 CQRS 命令。 +/// 命令通常用于修改系统状态。 +/// +/// 命令响应类型。 +public interface ICommand : IRequest; + +/// +/// 表示一个无显式返回值的 CQRS 命令。 +/// +public interface ICommand : ICommand; diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs similarity index 64% rename from GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs index 5ec607e5..9f5be0b6 100644 --- a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs @@ -1,7 +1,7 @@ -namespace GFramework.Core.Abstractions.Cqrs.Command; +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; /// /// 命令输入接口,定义命令模式中输入数据的契约 /// 该接口作为标记接口使用,不包含任何成员定义 /// -public interface ICommandInput : IInput; \ No newline at end of file +public interface ICommandInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs new file mode 100644 index 00000000..51323d19 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/IStreamCommand.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Cqrs.Abstractions.Cqrs.Command; + +/// +/// 表示一个流式 CQRS 命令。 +/// +/// 流式响应元素类型。 +public interface IStreamCommand : IStreamRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs new file mode 100644 index 00000000..09fc355e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs @@ -0,0 +1,13 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义 CQRS runtime 在分发期间携带的最小上下文标记。 +/// +/// +/// 该接口当前刻意保持为轻量 marker seam,只用于让 从 +/// GFramework.Core.AbstractionsIArchitectureContext 解耦。 +/// 运行时实现仍可在需要时识别更具体的上下文类型,并对现有 IContextAware 处理器执行兼容注入。 +/// +public interface ICqrsContext +{ +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs new file mode 100644 index 00000000..39635d3f --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义 CQRS 处理器程序集接入的 runtime seam。 +/// 该抽象负责承接“生成注册器优先、反射扫描回退”的处理器注册流程, +/// 让容器与架构启动链不再直接依赖固定的注册实现类型。 +/// +public interface ICqrsHandlerRegistrar +{ + /// + /// 扫描并注册指定程序集集合中的 CQRS 处理器。 + /// + /// 要接入的程序集集合。 + void RegisterHandlers(IEnumerable assemblies); +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs new file mode 100644 index 00000000..632af83e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs @@ -0,0 +1,81 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义架构上下文使用的 CQRS runtime seam。 +/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦, +/// 使 CQRS runtime 契约可独立归属到 GFramework.Cqrs.Abstractions。 +/// +public interface ICqrsRuntime +{ + /// + /// 发送请求并返回响应。 + /// + /// 响应类型。 + /// 当前 CQRS 分发上下文。 + /// 要分发的请求。 + /// 取消令牌。 + /// 请求响应。 + /// + /// 。 + /// + /// + /// 当前上下文无法满足运行时要求,例如未找到对应请求处理器,或请求处理链中的 + /// IContextAware 对象需要 IArchitectureContext 但当前 不提供该能力。 + /// + /// + /// 该契约允许调用方传入任意 , + /// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 IArchitectureContext。 + /// + ValueTask SendAsync( + ICqrsContext context, + IRequest request, + CancellationToken cancellationToken = default); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前 CQRS 分发上下文。 + /// 要发布的通知。 + /// 取消令牌。 + /// 表示通知分发完成的值任务。 + /// + /// 。 + /// + /// + /// 已解析到的通知处理器需要框架级上下文注入,但当前 不提供 + /// IArchitectureContext 能力。 + /// + /// + /// 默认实现允许零处理器场景静默完成;只有在处理器注入前置条件不满足时才会抛出异常。 + /// + ValueTask PublishAsync( + ICqrsContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification; + + /// + /// 创建流式请求的异步响应序列。 + /// + /// 流元素类型。 + /// 当前 CQRS 分发上下文。 + /// 流式请求。 + /// 取消令牌。 + /// 按需生成的异步响应序列。 + /// + /// 。 + /// + /// + /// 当前上下文无法满足运行时要求,例如未找到对应流式处理器,或流式处理链中的 + /// IContextAware 对象需要 IArchitectureContext 但当前 不提供该能力。 + /// + /// + /// 返回的异步序列在枚举前通常已完成处理器解析与上下文注入, + /// 因此调用方应把 视为整个枚举生命周期内的必需依赖。 + /// + IAsyncEnumerable CreateStream( + ICqrsContext context, + IStreamRequest request, + CancellationToken cancellationToken = default); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs similarity index 91% rename from GFramework.Core.Abstractions/Cqrs/IInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IInput.cs index dfed5012..a8b6dbcb 100644 --- a/GFramework.Core.Abstractions/Cqrs/IInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs; +namespace GFramework.Cqrs.Abstractions.Cqrs; /// /// 表示输入数据的标记接口。 /// 该接口用于标识各类CQRS模式中的输入参数类型。 /// -public interface IInput; \ No newline at end of file +public interface IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs new file mode 100644 index 00000000..4a2dbb68 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs @@ -0,0 +1,7 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示一个一对多发布的通知消息。 +/// 通知不要求返回值,允许被零个或多个处理器消费。 +/// +public interface INotification; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs new file mode 100644 index 00000000..e3a007c8 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs @@ -0,0 +1,17 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示处理通知消息的处理器契约。 +/// +/// 通知类型。 +public interface INotificationHandler + where TNotification : INotification +{ + /// + /// 处理通知消息。 + /// + /// 要处理的通知。 + /// 取消令牌。 + /// 异步处理任务。 + ValueTask Handle(TNotification notification, CancellationToken cancellationToken); +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs new file mode 100644 index 00000000..11423c40 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs @@ -0,0 +1,22 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 定义 CQRS 请求处理前后的管道行为。 +/// +/// 请求类型。 +/// 响应类型。 +public interface IPipelineBehavior + where TRequest : IRequest +{ + /// + /// 处理当前请求,并决定是否继续调用后续行为或最终处理器。 + /// + /// 当前请求消息。 + /// 下一个处理委托。 + /// 取消令牌。 + /// 请求响应。 + ValueTask Handle( + TRequest message, + MessageHandlerDelegate next, + CancellationToken cancellationToken); +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs new file mode 100644 index 00000000..dd6abb62 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs @@ -0,0 +1,8 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示一个有响应的 CQRS 请求。 +/// 该接口是命令、查询以及其他请求语义的统一基接口。 +/// +/// 请求响应类型。 +public interface IRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs new file mode 100644 index 00000000..95cdd1d1 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示处理单个 CQRS 请求的处理器契约。 +/// +/// 请求类型。 +/// 响应类型。 +public interface IRequestHandler + where TRequest : IRequest +{ + /// + /// 处理指定请求并返回结果。 + /// + /// 要处理的请求。 + /// 取消令牌。 + /// 请求结果。 + ValueTask Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs new file mode 100644 index 00000000..5464459b --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs @@ -0,0 +1,8 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示一个流式 CQRS 请求。 +/// 请求处理器可以逐步产生响应序列,而不是一次性返回完整结果。 +/// +/// 流式响应元素类型。 +public interface IStreamRequest; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs new file mode 100644 index 00000000..44e7c79d --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示处理流式 CQRS 请求的处理器契约。 +/// +/// 流式请求类型。 +/// 流式响应元素类型。 +public interface IStreamRequestHandler + where TRequest : IStreamRequest +{ + /// + /// 处理流式请求并返回异步响应序列。 + /// + /// 要处理的请求。 + /// 取消令牌。 + /// 异步响应序列。 + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs new file mode 100644 index 00000000..8575ebd8 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs @@ -0,0 +1,19 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示 CQRS 请求在管道中继续向下执行的处理委托。 +/// +/// +/// 管道行为可以通过不调用该委托来短路请求处理。 +/// 除显式实现重试等高级语义外,行为通常应最多调用一次该委托,以维持单次请求分发的确定性。 +/// 调用方应传递当前收到的 ,确保取消信号沿整条管道一致传播。 +/// +/// 请求类型。 +/// 响应类型。 +/// 当前请求消息。 +/// 取消令牌。 +/// 请求响应。 +public delegate ValueTask MessageHandlerDelegate( + TRequest message, + CancellationToken cancellationToken) + where TRequest : IRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs similarity index 87% rename from GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs index 8b791839..4fb6f735 100644 --- a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs.Notification; +namespace GFramework.Cqrs.Abstractions.Cqrs.Notification; /// /// 表示通知输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中通知类型的输入参数。 /// -public interface INotificationInput : IInput; \ No newline at end of file +public interface INotificationInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs new file mode 100644 index 00000000..edf5e1a2 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs @@ -0,0 +1,8 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; + +/// +/// 表示一个 CQRS 查询。 +/// 查询用于读取数据,不应产生副作用。 +/// +/// 查询响应类型。 +public interface IQuery : IRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs similarity index 50% rename from GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs index c505c4ff..a17e7b6a 100644 --- a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs @@ -1,6 +1,6 @@ -namespace GFramework.Core.Abstractions.Cqrs.Query; +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; /// /// 查询输入接口,定义了查询操作的输入规范 /// -public interface IQueryInput : IInput; \ No newline at end of file +public interface IQueryInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs new file mode 100644 index 00000000..150fb32c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IStreamQuery.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Cqrs.Abstractions.Cqrs.Query; + +/// +/// 表示一个流式 CQRS 查询。 +/// +/// 流式响应元素类型。 +public interface IStreamQuery : IStreamRequest; diff --git a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs similarity index 88% rename from GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs index 0a0b1591..14f89b89 100644 --- a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace GFramework.Core.Abstractions.Cqrs.Request; +namespace GFramework.Cqrs.Abstractions.Cqrs.Request; /// /// 表示请求输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中请求类型的输入参数。 /// -public interface IRequestInput : IInput; \ No newline at end of file +public interface IRequestInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs new file mode 100644 index 00000000..57d053bc --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs @@ -0,0 +1,13 @@ +namespace GFramework.Cqrs.Abstractions.Cqrs; + +/// +/// 表示没有实际返回值的 CQRS 响应类型。 +/// 该类型用于统一命令与请求的泛型签名,避免引入外部库的 Unit 定义。 +/// +public readonly record struct Unit +{ + /// + /// 获取默认的空响应实例。 + /// + public static Unit Value { get; } = new(); +} diff --git a/GFramework.Cqrs.Abstractions/Directory.Build.props b/GFramework.Cqrs.Abstractions/Directory.Build.props new file mode 100644 index 00000000..9f372d0c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Directory.Build.props @@ -0,0 +1,18 @@ + + + netstandard2.1 + true + true + preview + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj new file mode 100644 index 00000000..8e07fd2c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + GeWuYou.$(AssemblyName) + true + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute + enable + true + + + diff --git a/GFramework.Cqrs.Abstractions/GlobalUsings.cs b/GFramework.Cqrs.Abstractions/GlobalUsings.cs new file mode 100644 index 00000000..5cd04a4e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs new file mode 100644 index 00000000..ccc6e27f --- /dev/null +++ b/GFramework.Cqrs.Tests/Coroutine/CqrsCoroutineExtensionsTests.cs @@ -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; + +/// +/// 的单元测试类。 +/// 验证新的 CQRS 协程扩展直接走框架内建 CQRS runtime, +/// 并确保协程对命令调度异常的传播行为保持稳定。 +/// +[TestFixture] +public class CqrsCoroutineExtensionsTests +{ + /// + /// 验证SendCommandCoroutine应该返回IEnumerator + /// + [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())) + .Returns(ValueTask.CompletedTask); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); + + Assert.That(coroutine, Is.InstanceOf>()); + } + + /// + /// 验证 SendCommandCoroutine 在底层命令调度失败时会重新抛出原始异常。 + /// + [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())) + .Returns(new ValueTask(Task.FromException(expectedException))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); + + Assert.That(coroutine.MoveNext(), Is.True); + var exception = Assert.Throws(() => coroutine.MoveNext()); + Assert.That(exception, Is.SameAs(expectedException)); + } + + /// + /// 验证 SendCommandCoroutine 在提供错误回调时也会传递解包后的原始异常, + /// 避免回调路径暴露 。 + /// + [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())) + .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)); + } + + /// + /// 验证 SendCommandCoroutine 在底层命令被取消且未提供错误回调时会抛出取消异常。 + /// + [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())) + .Returns(new ValueTask(Task.FromCanceled(cancellationTokenSource.Token))); + + var coroutine = CqrsCoroutineExtensions.SendCommandCoroutine(contextAware, command); + + Assert.That(coroutine.MoveNext(), Is.True); + Assert.Throws(() => coroutine.MoveNext()); + } + + /// + /// 验证 SendCommandCoroutine 在底层命令被取消且提供错误回调时会把取消异常转发给回调。 + /// + [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())) + .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()); + } + + /// + /// 测试用的简单命令类 + /// + private sealed record TestCommand(string Data) : IRequest; + + /// + /// 上下文感知基类的模拟实现 + /// + private sealed class TestContextAware : IContextAware + { + /// + /// 提供可配置的架构上下文 Mock。 + /// + public Mock MockContext { get; } = new(); + + /// + /// 获取当前架构上下文。 + /// + /// 用于 CQRS 调用的架构上下文实例。 + public IArchitectureContext GetContext() + { + return MockContext.Object; + } + + /// + /// 设置架构上下文。 + /// + /// 要设置的架构上下文。 + public void SetContext(IArchitectureContext context) + { + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs b/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs new file mode 100644 index 00000000..58e86f89 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/AbstractCqrsHandlerContextTests.cs @@ -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; + +/// +/// 验证 CQRS handler 基类在脱离 dispatcher 使用时会显式失败,并在注入上下文后保持可观察行为。 +/// +[TestFixture] +internal sealed class AbstractCqrsHandlerContextTests +{ + /// + /// 验证新的轻量 handler 基类不会再偷偷回退到全局 GameContext。 + /// + [Test] + public void GetContext_Should_Throw_When_Handler_Has_Not_Been_Initialized_By_Runtime() + { + var handler = new TestCommandHandler(); + + var exception = Assert.Throws(() => ((IContextAware)handler).GetContext()); + + Assert.That( + exception!.Message, + Does.Contain("has not been initialized").IgnoreCase); + } + + /// + /// 验证 runtime 注入上下文后,派生 handler 可以继续访问 Context 并收到 OnContextReady 回调。 + /// + [Test] + public async Task Handle_Should_Observe_Injected_Context_And_OnContextReady_Callback() + { + var handler = new TestCommandHandler(); + var context = new Mock(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)); + }); + } + + /// + /// 用于验证上下文注入行为的最小 CQRS 命令。 + /// + private sealed record TestCommand : ICommand; + + /// + /// 暴露基类上下文访问与初始化回调的测试处理器。 + /// + private sealed class TestCommandHandler : AbstractCommandHandler + { + public int OnContextReadyCallCount { get; private set; } + + public IArchitectureContext? LastObservedContext { get; private set; } + + protected override void OnContextReady() + { + OnContextReadyCallCount++; + } + + public override ValueTask Handle(TestCommand command, CancellationToken cancellationToken) + { + LastObservedContext = Context; + return ValueTask.FromResult(Unit.Value); + } + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs new file mode 100644 index 00000000..badd7490 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -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; + +/// +/// 验证 CQRS dispatcher 会缓存热路径中的服务类型与调用委托。 +/// +[TestFixture] +internal sealed class CqrsDispatcherCacheTests +{ + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + + /// + /// 初始化测试上下文。 + /// + [SetUp] + public void SetUp() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + _container = new MicrosoftDiContainer(); + _container.RegisterCqrsPipelineBehavior(); + + CqrsTestRuntime.RegisterHandlers( + _container, + typeof(CqrsDispatcherCacheTests).Assembly, + typeof(ArchitectureContext).Assembly); + + _container.Freeze(); + _context = new ArchitectureContext(_container); + ClearDispatcherCaches(); + } + + /// + /// 清理测试上下文引用。 + /// + [TearDown] + public void TearDown() + { + _context = null; + _container = null; + } + + /// + /// 验证相同消息类型重复分发时,不会重复扩张服务类型与调用委托缓存。 + /// + [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)); + }); + } + + /// + /// 验证 request 调用委托会按响应类型分别缓存,避免不同响应类型共用 object 结果桥接。 + /// + [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)); + }); + } + + /// + /// 通过反射读取 dispatcher 的静态缓存字典。 + /// + 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."); + } + + /// + /// 清空本测试依赖的 dispatcher 静态缓存,避免跨用例共享进程级状态导致断言漂移。 + /// + 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(); + } + + /// + /// 通过反射读取 dispatcher 嵌套泛型缓存类型上的静态缓存字典。 + /// + 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."); + } + + /// + /// 获取 CQRS dispatcher 运行时类型。 + /// + private static Type GetDispatcherType() + { + return typeof(CqrsReflectionFallbackAttribute).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!; + } + + /// + /// 消费整个异步流,确保建流路径被真实执行。 + /// + private static async Task DrainAsync(IAsyncEnumerable stream) + { + await foreach (var _ in stream) + { + } + } +} + +/// +/// 用于验证 request 服务类型缓存的测试请求。 +/// +internal sealed record DispatcherCacheRequest : IRequest; + +/// +/// 用于验证 notification 服务类型缓存的测试通知。 +/// +internal sealed record DispatcherCacheNotification : INotification; + +/// +/// 用于验证 stream 服务类型缓存的测试请求。 +/// +internal sealed record DispatcherCacheStreamRequest : IStreamRequest; + +/// +/// 用于验证 pipeline invoker 缓存的测试请求。 +/// +internal sealed record DispatcherPipelineCacheRequest : IRequest; + +/// +/// 用于验证按响应类型分层 request invoker 缓存的测试请求。 +/// +internal sealed record DispatcherStringCacheRequest : IRequest; + +/// +/// 处理 。 +/// +internal sealed class DispatcherCacheRequestHandler : IRequestHandler +{ + /// + /// 返回固定结果,供缓存测试验证 dispatcher 请求路径。 + /// + public ValueTask Handle(DispatcherCacheRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(1); + } +} + +/// +/// 处理 。 +/// +internal sealed class DispatcherCacheNotificationHandler : INotificationHandler +{ + /// + /// 消费通知,不执行额外副作用。 + /// + public ValueTask Handle(DispatcherCacheNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} + +/// +/// 处理 。 +/// +internal sealed class DispatcherCacheStreamHandler : IStreamRequestHandler +{ + /// + /// 返回一个最小流,供缓存测试命中 stream 分发路径。 + /// + public async IAsyncEnumerable Handle( + DispatcherCacheStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + yield return 1; + await Task.CompletedTask; + } +} + +/// +/// 处理 。 +/// +internal sealed class DispatcherPipelineCacheRequestHandler : IRequestHandler +{ + /// + /// 返回固定结果,供 pipeline 缓存测试使用。 + /// + public ValueTask Handle(DispatcherPipelineCacheRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(2); + } +} + +/// +/// 处理 。 +/// +internal sealed class DispatcherStringCacheRequestHandler : IRequestHandler +{ + /// + /// 返回固定字符串,供按响应类型缓存测试验证 string 路径。 + /// + public ValueTask Handle(DispatcherStringCacheRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult("dispatcher-cache"); + } +} + +/// +/// 为 提供最小 pipeline 行为, +/// 用于命中 dispatcher 的 pipeline invoker 缓存分支。 +/// +internal sealed class DispatcherPipelineCacheBehavior : IPipelineBehavior +{ + /// + /// 直接转发到下一个处理器。 + /// + public ValueTask Handle( + DispatcherPipelineCacheRequest request, + MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(request, cancellationToken); + } +} diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs new file mode 100644 index 00000000..b44b0bb1 --- /dev/null +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -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; + +/// +/// 验证 CQRS 处理器自动注册在顺序与容错层面的可观察行为。 +/// +[TestFixture] +internal sealed class CqrsHandlerRegistrarTests +{ + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + + /// + /// 初始化测试容器并重置共享状态。 + /// + [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); + } + + /// + /// 清理测试过程中创建的上下文与共享状态。 + /// + [TearDown] + public void TearDown() + { + _context = null; + _container = null; + DeterministicNotificationHandlerState.Reset(); + } + + /// + /// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。 + /// + [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) + ])); + } + + /// + /// 验证部分类型加载失败时仍能保留可加载类型,并记录诊断日志。 + /// + [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(); + 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>(); + 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; + } + } + + /// + /// 验证当程序集提供源码生成的注册器时,运行时会优先使用该注册器而不是反射扫描类型列表。 + /// + [Test] + public void RegisterHandlers_Should_Use_Generated_Registry_When_Available() + { + var generatedAssembly = new Mock(); + 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>(); + + Assert.That( + handlers.Select(static handler => handler.GetType()), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + } + + /// + /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 + /// + [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(); + 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>(); + 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; + } + } + + /// + /// 验证当生成注册器提供精确 fallback 类型名时,运行时会定向补扫剩余 handlers, + /// 而不是重新枚举整个程序集的类型列表。 + /// + [Test] + public void RegisterHandlers_Should_Use_Targeted_Type_Lookups_For_Reflection_Fallback_Without_Duplicates() + { + var generatedAssembly = new Mock(); + 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) && + 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); + } + + /// + /// 验证手写 fallback metadata 直接提供 handler 类型时,运行时会复用这些类型, + /// 而不会再通过程序集名称查找或整程序集扫描补齐映射。 + /// + [Test] + public void RegisterHandlers_Should_Use_Direct_Fallback_Types_Without_GetType_Or_GetTypes() + { + var generatedAssembly = new Mock(); + 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) && + 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); + } +} + +/// +/// 记录确定性通知处理器的实际执行顺序。 +/// +internal static class DeterministicNotificationHandlerState +{ + /// + /// 获取当前测试中的通知处理器执行顺序。 + /// + public static List InvocationOrder { get; } = []; + + /// + /// 重置共享的执行顺序状态。 + /// + public static void Reset() + { + InvocationOrder.Clear(); + } +} + +/// +/// 用于验证同一通知的多个处理器是否按稳定顺序执行。 +/// +internal sealed record DeterministicOrderNotification : INotification; + +/// +/// 故意放在 Alpha 之前声明,用于验证注册器不会依赖源码声明顺序。 +/// +internal sealed class ZetaDeterministicNotificationHandler : INotificationHandler +{ + /// + /// 记录当前处理器已执行。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + DeterministicNotificationHandlerState.InvocationOrder.Add(nameof(ZetaDeterministicNotificationHandler)); + return ValueTask.CompletedTask; + } +} + +/// +/// 名称排序上应先于 Zeta 处理器执行的通知处理器。 +/// +internal sealed class AlphaDeterministicNotificationHandler : INotificationHandler +{ + /// + /// 记录当前处理器已执行。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken) + { + DeterministicNotificationHandlerState.InvocationOrder.Add(nameof(AlphaDeterministicNotificationHandler)); + return ValueTask.CompletedTask; + } +} + +/// +/// 为 CQRS 注册测试捕获真实启动路径中创建的日志记录器。 +/// +/// +/// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。 +/// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。 +/// +internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider +{ + private readonly List _loggers = []; + + /// + /// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。 + /// + /// 要应用到新建测试日志器的最小日志级别。 + public CapturingLoggerFactoryProvider(LogLevel minLevel = LogLevel.Info) + { + MinLevel = minLevel; + } + + /// + /// 获取通过当前提供程序创建的全部测试日志器。 + /// + public IReadOnlyList Loggers => _loggers; + + /// + /// 获取或设置新建测试日志器的最小日志级别。 + /// + public LogLevel MinLevel { get; set; } + + /// + /// 创建一个测试日志器并将其纳入捕获集合。 + /// + /// 日志记录器名称。 + /// 用于后续断言的测试日志器。 + public ILogger CreateLogger(string name) + { + var logger = new TestLogger(name, MinLevel); + _loggers.Add(logger); + return logger; + } +} + +/// +/// 用于验证生成注册器路径的通知消息。 +/// +internal sealed record GeneratedRegistryNotification : INotification; + +/// +/// 由模拟的源码生成注册器显式注册的通知处理器。 +/// +internal sealed class GeneratedRegistryNotificationHandler : INotificationHandler +{ + /// + /// 处理生成注册器测试中的通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} + +/// +/// 模拟源码生成器为某个程序集生成的 CQRS 处理器注册器。 +/// +internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将测试通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} + +/// +/// 用于验证“生成注册器 + reflection fallback”组合路径的私有嵌套处理器容器。 +/// +internal sealed class ReflectionFallbackNotificationContainer +{ + /// + /// 获取仅能通过反射补扫接入的私有嵌套处理器类型。 + /// + public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler); + + private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler + : INotificationHandler + { + /// + /// 处理测试通知。 + /// + /// 通知实例。 + /// 取消令牌。 + /// 已完成任务。 + public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + } +} + +/// +/// 模拟局部生成注册器场景中,仅注册“可由生成代码直接引用”的那部分 handlers。 +/// +internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry +{ + /// + /// 将生成路径可见的通知处理器注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + logger.Debug( + $"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler).FullName}."); + } +} diff --git a/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj new file mode 100644 index 00000000..796883e4 --- /dev/null +++ b/GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + $(TestTargetFrameworks) + disable + enable + false + true + + + + + + + + + + + + + + + + + diff --git a/GFramework.Cqrs.Tests/GlobalUsings.cs b/GFramework.Cqrs.Tests/GlobalUsings.cs new file mode 100644 index 00000000..c47473b0 --- /dev/null +++ b/GFramework.Cqrs.Tests/GlobalUsings.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +global using System; +global using System.Collections; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Linq; +global using System.Reflection; +global using System.Runtime.CompilerServices; +global using System.Threading; +global using System.Threading.Tasks; +global using GFramework.Tests.Common; +global using Microsoft.Extensions.DependencyInjection; +global using Moq; +global using NUnit.Compatibility; +global using NUnit.Framework; diff --git a/GFramework.Cqrs.Tests/Logging/TestLogger.cs b/GFramework.Cqrs.Tests/Logging/TestLogger.cs new file mode 100644 index 00000000..c0432bc7 --- /dev/null +++ b/GFramework.Cqrs.Tests/Logging/TestLogger.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace GFramework.Cqrs.Tests.Logging; + +/// +/// 供 CQRS 测试项目复用的最小日志记录器实现。 +/// +public sealed class TestLogger : AbstractLogger +{ + /// + /// 初始化测试日志记录器。 + /// + /// 日志名称。 + /// 最小日志级别。 + public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base(name, minLevel) + { + } + + /// + /// 获取当前测试期间捕获到的日志条目。 + /// + public List Logs { get; } = []; + + /// + /// 将日志写入内存,供断言使用。 + /// + /// 日志级别。 + /// 日志消息。 + /// 关联异常。 + protected override void Write(LogLevel level, string message, Exception? exception) + { + Logs.Add(new LogEntry(level, message, exception)); + } + + /// + /// 表示单条测试日志记录。 + /// + /// 日志级别。 + /// 日志消息。 + /// 关联异常。 + public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception); +} diff --git a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs similarity index 95% rename from GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index bc5cd782..fe8f4413 100644 --- a/GFramework.Core.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -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; /// /// 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 { - private static bool _circuitOpen = false; - public ValueTask 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= 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; } + + /// + /// 重置断路器测试状态,避免静态字段在测试之间互相污染。 + /// + public static void Reset() + { + FailureCount = 0; + SuccessCount = 0; + CircuitOpen = false; + } } public sealed record TestCircuitBreakerRequest : IRequest @@ -487,4 +492,4 @@ public sealed record TestDatabaseRequest : IRequest public List Storage { get; init; } = new(); } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs similarity index 87% rename from GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index 85bbf3aa..728f005a 100644 --- a/GFramework.Core.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -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; /// /// 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 +/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。 +/// +public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase, + IRequestHandler +{ + private static int _nextInstanceId; + private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId); + + public static List Contexts { get; } = []; + public static List SeenInstanceIds { get; } = []; + + /// + /// 记录当前实例编号与收到的架构上下文。 + /// + /// 请求实例。 + /// 取消令牌。 + /// 当前处理器实例编号。 + public ValueTask Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken) + { + Contexts.Add(Context); + SeenInstanceIds.Add(_instanceId); + return ValueTask.FromResult(_instanceId); + } + + /// + /// 重置跨测试共享的实例跟踪状态。 + /// + public static void Reset() + { + Contexts.Clear(); + SeenInstanceIds.Clear(); + _nextInstanceId = 0; + } +} + public sealed record TestContextAwareRequest : IRequest; public static class TestContextAwareHandler @@ -545,6 +595,11 @@ public sealed record TestMediatorRequest : IRequest public int Value { get; init; } } +/// +/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。 +/// +public sealed record TestPerDispatchContextAwareRequest : IRequest; + // 传统命令用于混合测试 public class TestTraditionalCommand : ICommand { @@ -559,4 +614,4 @@ public class TestTraditionalCommand : ICommand public IArchitectureContext GetContext() => null!; } -#endregion \ No newline at end of file +#endregion diff --git a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs similarity index 93% rename from GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs rename to GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index 020bc26b..423b1c9b 100644 --- a/GFramework.Core.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -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 { /// /// 测试初始化方法,在每个测试方法执行前运行。 - /// 负责初始化日志工厂、依赖注入容器、Mediator以及各种总线服务。 + /// 负责初始化日志工厂、依赖注入容器、自有 CQRS 处理器以及各种总线服务。 /// [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; /// /// 测试SendRequestAsync方法在请求有效时返回结果 @@ -194,19 +186,19 @@ public class MediatorComprehensiveTests /// - /// 测试未注册的Mediator抛出InvalidOperationException + /// 测试未注册的 CQRS handler 时抛出 InvalidOperationException /// [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(async () => - await contextWithoutMediator.SendRequestAsync(testRequest)); + await contextWithoutHandlers.SendRequestAsync(testRequest)); } /// @@ -270,10 +262,10 @@ public class MediatorComprehensiveTests } /// - /// 测试并发Mediator请求不会相互干扰 + /// 测试并发 CQRS 请求不会相互干扰 /// [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>(); @@ -389,10 +381,10 @@ public class MediatorComprehensiveTests } /// - /// 测试Mediator性能基准 + /// 测试 CQRS 性能基准 /// [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 } /// - /// 测试Mediator和传统CQRS可以共存 + /// 测试自有 CQRS 和传统 CQRS 可以共存 /// [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 { @@ -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 { public int Value { get; init; } @@ -662,7 +654,7 @@ public sealed record TestStreamRequest : IStreamRequest public int[] Values { get; init; } = []; } -// ✅ 这些 Handler 使用 Mediator.IRequestHandler +// ✅ 这些 Handler 使用自有 CQRS IRequestHandler public sealed class TestRequestHandler : IRequestHandler { public ValueTask Handle(TestRequest request, CancellationToken cancellationToken) @@ -726,4 +718,4 @@ public sealed class TestStreamRequestHandler : IStreamRequestHandler -/// 表示一个基础命令类,用于处理带有输入和响应的命令模式实现。 -/// 该类实现了 ICommand<TResponse> 接口,提供了通用的命令结构。 +/// 为携带输入模型的 CQRS 命令提供统一基类。 /// -/// 命令输入数据的类型 -/// 命令执行后返回结果的类型 -/// 命令执行所需的输入数据 -public abstract class CommandBase(TInput input) : ICommand where TInput : ICommandInput +/// 命令输入类型。 +/// 命令响应类型。 +/// 命令执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class CommandBase(TInput input) : ICommand + where TInput : ICommandInput { /// - /// 获取命令的输入数据。 + /// 获取命令执行时携带的输入对象。 /// public TInput Input => input; -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs similarity index 70% rename from GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs rename to GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs index 4a56c775..63896c92 100644 --- a/GFramework.Core/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs @@ -11,19 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Logging; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Behaviors; +namespace GFramework.Cqrs.Cqrs.Behaviors; /// -/// 日志记录行为类,用于在CQRS管道中记录请求处理的日志信息 -/// 实现IPipelineBehavior接口,为请求处理提供日志记录功能 +/// 在 CQRS 请求管道中记录请求开始、完成、取消与失败日志。 /// -/// 请求类型,必须实现IRequest接口 -/// 响应类型 +/// 请求类型。 +/// 响应类型。 +/// +/// 该行为已迁移到 GFramework.Cqrs.Cqrs.Behaviors 命名空间, +/// 实现位于 GFramework.Cqrs 程序集,用于承载 CQRS runtime 细节并与旧层解耦。 +/// public sealed class LoggingBehavior : IPipelineBehavior where TRequest : IRequest { @@ -31,13 +32,12 @@ public sealed class LoggingBehavior : IPipelineBehavior)); /// - /// 处理请求并记录日志 - /// 在请求处理前后记录调试信息,处理异常时记录错误日志 + /// 执行日志包装后的下一段请求处理逻辑。 /// - /// 要处理的请求消息 - /// 下一个处理委托,用于继续管道执行 - /// 取消令牌,用于取消操作 - /// 处理结果的ValueTask + /// 当前请求消息。 + /// 后续处理委托。 + /// 取消令牌。 + /// 请求处理结果。 public async ValueTask Handle( TRequest message, MessageHandlerDelegate next, @@ -69,4 +69,4 @@ public sealed class LoggingBehavior : IPipelineBehavior -/// 性能监控行为类,用于监控CQRS请求的执行时间 -/// 实现IPipelineBehavior接口,检测并记录执行时间过长的请求 +/// 在 CQRS 请求管道中监控处理耗时,并对长耗时请求发出告警。 /// -/// 请求类型,必须实现IRequest接口 -/// 响应类型 +/// 请求类型。 +/// 响应类型。 +/// +/// 该行为保留现有公开命名空间以维持消费端兼容性,但实现已迁入 GFramework.Cqrs 程序集。 +/// public sealed class PerformanceBehavior : IPipelineBehavior where TRequest : IRequest { + private const double SlowRequestThresholdMilliseconds = 500; + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PerformanceBehavior)); /// - /// 处理请求并监控执行时间 - /// 使用Stopwatch测量请求处理耗时,超过500ms时记录警告日志 + /// 统计当前请求处理耗时,并在超阈值时记录警告日志。 /// - /// 要处理的请求消息 - /// 下一个处理委托,用于继续管道执行 - /// 取消令牌,用于取消操作 - /// 处理结果的ValueTask + /// 当前请求消息。 + /// 后续处理委托。 + /// 取消令牌。 + /// 请求处理结果。 public async ValueTask Handle( TRequest message, MessageHandlerDelegate next, @@ -53,12 +54,11 @@ public sealed class PerformanceBehavior : IPipelineBehavior { var elapsed = Stopwatch.GetElapsedTime(start); - if (elapsed.TotalMilliseconds > 500) + if (elapsed.TotalMilliseconds > SlowRequestThresholdMilliseconds) { var requestName = typeof(TRequest).Name; - _logger.Warn( - $"Long Running Request: {requestName} ({elapsed.TotalMilliseconds:F2} ms)"); + _logger.Warn($"Long Running Request: {requestName} ({elapsed.TotalMilliseconds:F2} ms)"); } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs b/GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs similarity index 72% rename from GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs rename to GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs index 8f7b6112..825737bf 100644 --- a/GFramework.Core/Cqrs/Command/AbstractCommandHandler.cs +++ b/GFramework.Cqrs/Cqrs/Command/AbstractCommandHandler.cs @@ -11,17 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; -namespace GFramework.Core.Cqrs.Command; +namespace GFramework.Cqrs.Cqrs.Command; /// /// 抽象命令处理器基类 -/// 继承自ContextAwareBase并实现ICommandHandler接口,为具体的命令处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 命令类型 -public abstract class AbstractCommandHandler : ContextAwareBase, ICommandHandler +public abstract class AbstractCommandHandler : CqrsContextAwareHandlerBase, IRequestHandler where TCommand : ICommand { /// @@ -36,12 +37,13 @@ public abstract class AbstractCommandHandler : ContextAwareBase, IComm /// /// 抽象命令处理器基类(带返回值版本) -/// 继承自ContextAwareBase并实现ICommandHandler接口,为具体的命令处理器提供基础功能 -/// 支持泛型命令和结果类型,实现CQRS模式中的命令处理 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的命令处理器提供基础功能。 +/// 支持泛型命令和结果类型,框架会在每次分发前注入当前架构上下文。 /// /// 命令类型,必须实现ICommand接口 /// 命令执行结果类型 -public abstract class AbstractCommandHandler : ContextAwareBase, ICommandHandler +public abstract class AbstractCommandHandler : CqrsContextAwareHandlerBase, + IRequestHandler where TCommand : ICommand { /// @@ -52,4 +54,4 @@ public abstract class AbstractCommandHandler : ContextAwareBa /// 取消令牌,用于取消操作 /// 表示异步操作完成的ValueTask,包含命令执行结果 public abstract ValueTask Handle(TCommand command, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs b/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs new file mode 100644 index 00000000..cb4ccb6b --- /dev/null +++ b/GFramework.Cqrs/Cqrs/Command/AbstractStreamCommandHandler.cs @@ -0,0 +1,46 @@ +// 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; +using GFramework.Cqrs.Abstractions.Cqrs.Command; + +namespace GFramework.Cqrs.Cqrs.Command; + +/// +/// 抽象流式命令处理器基类。 +/// 继承自轻量 CQRS 上下文基类并实现 , +/// 为具体的流式命令处理器提供基础功能。 +/// +/// 流式命令类型,必须实现 +/// 流式命令响应元素类型。 +/// +/// 框架会在每次调用 CreateStream 进入实际处理逻辑前,为当前处理器实例注入架构上下文, +/// 因此派生类只能在 执行期间及其返回的异步枚举序列内假定 Context 可用。 +/// 默认注册器会将流式命令处理器注册为瞬态服务,以避免同一个上下文感知实例在多个流或并发请求之间复用。 +/// 派生类不应缓存处理器实例,也不应把依赖当前上下文的可变状态泄漏到流外部。 +/// 传入 的取消令牌同时约束流的创建与后续枚举, +/// 派生类应在启动阶段和每次生成响应前尊重取消请求,避免在调用方停止枚举后继续执行后台工作。 +/// +public abstract class AbstractStreamCommandHandler : CqrsContextAwareHandlerBase, + IStreamRequestHandler + where TCommand : IStreamCommand +{ + /// + /// 处理流式命令并返回异步可枚举的响应序列。 + /// 由具体的流式命令处理器子类实现流式处理逻辑。 + /// + /// 要处理的流式命令对象。 + /// 取消令牌,用于取消流式处理操作。 + /// 异步可枚举的响应序列,每个元素类型为 + public abstract IAsyncEnumerable Handle(TCommand command, CancellationToken cancellationToken); +} diff --git a/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs b/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs new file mode 100644 index 00000000..73e25ff1 --- /dev/null +++ b/GFramework.Cqrs/Cqrs/CqrsContextAwareHandlerBase.cs @@ -0,0 +1,59 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Rule; + +namespace GFramework.Cqrs.Cqrs; + +/// +/// 为 CQRS 处理器提供最小化的上下文感知基类实现。 +/// +/// +/// 该基类只承接 CQRS runtime 在分发前注入的 , +/// 不再像 ContextAwareBase 那样回退到 GameContext 全局查找。 +/// 这样可以让 GFramework.Cqrs 保持对 GFramework.Core 运行时实现的零依赖, +/// 同时在处理器被错误地脱离 dispatcher 使用时以显式异常快速失败。 +/// +public abstract class CqrsContextAwareHandlerBase : IContextAware +{ + private IArchitectureContext? _context; + + /// + /// 获取当前分发周期内已注入的架构上下文。 + /// + /// + /// 当前处理器尚未被 CQRS runtime 注入上下文。 + /// + protected IArchitectureContext Context => _context ?? throw new InvalidOperationException( + "The CQRS handler context has not been initialized. Ensure the handler is invoked through the CQRS runtime."); + + /// + /// 由 runtime 在分发前注入当前架构上下文。 + /// + /// 当前架构上下文。 + void IContextAware.SetContext(IArchitectureContext context) + { + ArgumentNullException.ThrowIfNull(context); + + _context = context; + OnContextReady(); + } + + /// + /// 获取当前处理器实例已绑定的架构上下文。 + /// + /// 当前分发周期内的架构上下文。 + IArchitectureContext IContextAware.GetContext() + { + return Context; + } + + /// + /// 当上下文注入完成后执行额外初始化。 + /// + /// + /// 该钩子保留与旧 ContextAwareBase 相近的扩展点, + /// 便于处理器在迁移后继续承接分发前的派生类初始化逻辑。 + /// + protected virtual void OnContextReady() + { + } +} diff --git a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs b/GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs similarity index 82% rename from GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs rename to GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs index de4772fb..16246d16 100644 --- a/GFramework.Core/Cqrs/Notification/AbstractNotificationHandler.cs +++ b/GFramework.Cqrs/Cqrs/Notification/AbstractNotificationHandler.cs @@ -11,18 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Notification; +namespace GFramework.Cqrs.Cqrs.Notification; /// /// 抽象通知处理器基类 -/// 继承自ContextAwareBase并实现INotificationHandler接口,为具体的通知处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现INotificationHandler接口,为具体的通知处理器提供基础功能 /// 用于处理CQRS模式中的通知消息,支持异步处理 /// /// 通知类型,必须实现INotification接口 -public abstract class AbstractNotificationHandler : ContextAwareBase, INotificationHandler +public abstract class AbstractNotificationHandler : CqrsContextAwareHandlerBase, + INotificationHandler where TNotification : INotification { /// @@ -33,4 +33,4 @@ public abstract class AbstractNotificationHandler : ContextAwareB /// 取消令牌,用于取消操作 /// 表示异步操作完成的ValueTask public abstract ValueTask Handle(TNotification notification, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs similarity index 72% rename from GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs rename to GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs index 4ce887cf..5096f4b7 100644 --- a/GFramework.Core/Cqrs/Query/AbstractQueryHandler.cs +++ b/GFramework.Cqrs/Cqrs/Query/AbstractQueryHandler.cs @@ -11,19 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Query; -namespace GFramework.Core.Cqrs.Query; +namespace GFramework.Cqrs.Cqrs.Query; /// /// 抽象查询处理器基类 -/// 继承自ContextAwareBase并实现IQueryHandler接口,为具体的查询处理器提供基础功能 -/// 支持泛型查询和结果类型,实现CQRS模式中的查询处理 +/// 继承自轻量 CQRS 上下文基类并实现 IRequestHandler 接口,为具体的查询处理器提供基础功能。 +/// 框架会在每次分发前注入当前架构上下文,因此派生类可以通过 Context 访问架构级服务。 /// /// 查询类型,必须实现IQuery接口 /// 查询结果类型 -public abstract class AbstractQueryHandler : ContextAwareBase, IQueryHandler +public abstract class AbstractQueryHandler : CqrsContextAwareHandlerBase, + IRequestHandler where TQuery : IQuery { /// @@ -34,4 +35,4 @@ public abstract class AbstractQueryHandler : ContextAwareBase, /// 取消令牌,用于取消操作 /// 表示异步操作完成的ValueTask,包含查询结果 public abstract ValueTask Handle(TQuery query, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs new file mode 100644 index 00000000..f532f80d --- /dev/null +++ b/GFramework.Cqrs/Cqrs/Query/AbstractStreamQueryHandler.cs @@ -0,0 +1,40 @@ +// 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; +using GFramework.Cqrs.Abstractions.Cqrs.Query; + +namespace GFramework.Cqrs.Cqrs.Query; + +/// +/// 为流式查询处理器提供共享的 CQRS 上下文访问基类。 +/// +/// 流式查询类型,必须实现 +/// 流式查询响应元素类型。 +/// +/// 该基类复用 的上下文注入能力,并实现 +/// 契约,让派生类只需聚焦于结果流的生成逻辑。 +/// 适用于需要逐步产出大量结果或长生命周期响应流的查询场景。 +/// +public abstract class AbstractStreamQueryHandler : CqrsContextAwareHandlerBase, + IStreamRequestHandler + where TQuery : IStreamQuery +{ + /// + /// 处理流式查询并返回异步可枚举的响应序列。 + /// + /// 要处理的流式查询对象。 + /// 用于停止结果流生成的取消令牌。 + /// 按需生成的异步响应序列。 + public abstract IAsyncEnumerable Handle(TQuery query, CancellationToken cancellationToken); +} diff --git a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs b/GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs similarity index 84% rename from GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs rename to GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs index 88e9efad..8343576b 100644 --- a/GFramework.Core/Cqrs/Request/AbstractRequestHandler.cs +++ b/GFramework.Cqrs/Cqrs/Request/AbstractRequestHandler.cs @@ -11,17 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Cqrs.Request; /// /// 抽象请求处理器基类,用于处理不返回具体响应的请求 -/// 继承自ContextAwareBase并实现IRequestHandler接口 +/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[Unit]接口 -public abstract class AbstractRequestHandler : ContextAwareBase, IRequestHandler +public abstract class AbstractRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler where TRequest : IRequest { /// @@ -35,11 +34,11 @@ public abstract class AbstractRequestHandler : ContextAwareBase, IRequ /// /// 抽象请求处理器基类,用于处理需要返回具体响应的请求 -/// 继承自ContextAwareBase并实现IRequestHandler接口 +/// 继承自轻量 CQRS 上下文基类并实现IRequestHandler接口 /// /// 请求类型,必须实现IRequest[TResponse]接口 /// 响应类型 -public abstract class AbstractRequestHandler : ContextAwareBase, +public abstract class AbstractRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler where TRequest : IRequest { /// @@ -49,4 +48,4 @@ public abstract class AbstractRequestHandler : ContextAware /// 取消令牌,用于取消操作 /// 表示异步操作的ValueTask,完成时返回处理结果 public abstract ValueTask Handle(TRequest request, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs b/GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs similarity index 86% rename from GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs rename to GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs index a6151b49..42b968f4 100644 --- a/GFramework.Core/Cqrs/Request/AbstractStreamRequestHandler.cs +++ b/GFramework.Cqrs/Cqrs/Request/AbstractStreamRequestHandler.cs @@ -11,19 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Rule; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; -namespace GFramework.Core.Cqrs.Request; +namespace GFramework.Cqrs.Cqrs.Request; /// /// 抽象流式请求处理器基类 -/// 继承自ContextAwareBase并实现IStreamRequestHandler接口,为具体的流式请求处理器提供基础功能 +/// 继承自轻量 CQRS 上下文基类并实现IStreamRequestHandler接口,为具体的流式请求处理器提供基础功能 /// 支持流式处理请求并产生异步可枚举的响应序列,适用于需要逐步返回结果的请求处理场景 /// /// 流式请求类型,必须实现IStreamRequest接口 /// 流式请求响应元素类型 -public abstract class AbstractStreamRequestHandler : ContextAwareBase, +public abstract class AbstractStreamRequestHandler : CqrsContextAwareHandlerBase, IStreamRequestHandler where TRequest : IStreamRequest { @@ -35,4 +34,4 @@ public abstract class AbstractStreamRequestHandler : Contex /// 取消令牌,用于取消流式请求操作 /// 异步可枚举的响应序列,每个元素类型为TResponse public abstract IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs b/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs new file mode 100644 index 00000000..a64875f3 --- /dev/null +++ b/GFramework.Cqrs/CqrsHandlerRegistryAttribute.cs @@ -0,0 +1,18 @@ +namespace GFramework.Cqrs; + +/// +/// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。 +/// +/// +/// 该特性通常由源码生成器自动添加到消费端程序集。 +/// 运行时读取到该特性后,会优先实例化对应的 , +/// 以常量时间获取处理器注册映射,而不是遍历程序集中的全部类型。 +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public sealed class CqrsHandlerRegistryAttribute(Type registryType) : Attribute +{ + /// + /// 获取承载 CQRS 处理器注册逻辑的注册器类型。 + /// + public Type RegistryType { get; } = registryType ?? throw new ArgumentNullException(nameof(registryType)); +} diff --git a/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs new file mode 100644 index 00000000..da557d84 --- /dev/null +++ b/GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs @@ -0,0 +1,71 @@ +namespace GFramework.Cqrs; + +/// +/// 标记程序集中的 CQRS 生成注册器仍需要运行时补充反射扫描。 +/// +/// +/// 该特性通常由源码生成器自动添加到消费端程序集。 +/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描, +/// 以覆盖那些生成代码无法直接引用的 handler 类型。 +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class CqrsReflectionFallbackAttribute : Attribute +{ + /// + /// 初始化 ,保留旧版“仅标记需要补扫”的语义。 + /// + public CqrsReflectionFallbackAttribute() + { + FallbackHandlerTypeNames = []; + FallbackHandlerTypes = []; + } + + /// + /// 初始化 。 + /// + /// + /// 需要运行时补充反射注册的处理器类型全名。 + /// 当该清单为空时,运行时会回退到整程序集扫描,以兼容旧版 marker 语义。 + /// + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) + { + ArgumentNullException.ThrowIfNull(fallbackHandlerTypeNames); + + FallbackHandlerTypeNames = fallbackHandlerTypeNames + .Where(static typeName => !string.IsNullOrWhiteSpace(typeName)) + .Distinct(StringComparer.Ordinal) + .OrderBy(static typeName => typeName, StringComparer.Ordinal) + .ToArray(); + FallbackHandlerTypes = []; + } + + /// + /// 初始化 。 + /// + /// + /// 需要运行时补充反射注册的处理器类型。 + /// 该重载适合手写或第三方程序集显式声明可直接引用的 fallback handlers, + /// 避免再通过字符串名称回查程序集元数据。 + /// + public CqrsReflectionFallbackAttribute(params Type[] fallbackHandlerTypes) + { + ArgumentNullException.ThrowIfNull(fallbackHandlerTypes); + + FallbackHandlerTypeNames = []; + FallbackHandlerTypes = fallbackHandlerTypes + .Where(static type => type is not null) + .Distinct() + .OrderBy(static type => type.FullName ?? type.Name, StringComparer.Ordinal) + .ToArray(); + } + + /// + /// 获取需要运行时补充反射注册的处理器类型全名集合。 + /// + public IReadOnlyList FallbackHandlerTypeNames { get; } + + /// + /// 获取可直接供运行时补充反射注册的处理器类型集合。 + /// + public IReadOnlyList FallbackHandlerTypes { get; } +} diff --git a/GFramework.Cqrs/CqrsRuntimeFactory.cs b/GFramework.Cqrs/CqrsRuntimeFactory.cs new file mode 100644 index 00000000..cbed68aa --- /dev/null +++ b/GFramework.Cqrs/CqrsRuntimeFactory.cs @@ -0,0 +1,67 @@ +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Internal; + +namespace GFramework.Cqrs; + +/// +/// 提供 CQRS runtime 默认实现的跨程序集创建入口。 +/// +/// +/// 需要在不暴露内部实现细节的前提下接入默认 CQRS runtime, +/// 因此通过该工厂返回抽象接口,而不是直接公开内部 dispatcher / registrar 类型。 +/// +public static class CqrsRuntimeFactory +{ + /// + /// 创建默认 CQRS runtime 分发器。 + /// + /// 目标依赖注入容器。 + /// 用于 runtime 诊断的日志器。 + /// 默认 CQRS runtime。 + /// + /// 。 + /// + public static ICqrsRuntime CreateRuntime(IIocContainer container, ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(logger); + + return new CqrsDispatcher(container, logger); + } + + /// + /// 创建默认 CQRS 处理器注册器。 + /// + /// 目标依赖注入容器。 + /// 用于注册阶段诊断的日志器。 + /// 默认 CQRS handler registrar。 + /// + /// 。 + /// + public static ICqrsHandlerRegistrar CreateHandlerRegistrar(IIocContainer container, ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(logger); + + return new DefaultCqrsHandlerRegistrar(container, logger); + } + + /// + /// 创建默认的 CQRS 程序集注册协调器。 + /// + /// 底层 handler 注册器。 + /// 用于注册阶段诊断的日志器。 + /// 默认 CQRS 程序集注册协调器。 + /// + /// 。 + /// + public static ICqrsRegistrationService CreateRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger) + { + ArgumentNullException.ThrowIfNull(registrar); + ArgumentNullException.ThrowIfNull(logger); + + return new DefaultCqrsRegistrationService(registrar, logger); + } +} diff --git a/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs new file mode 100644 index 00000000..b3c2bde8 --- /dev/null +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsCommandExtensions.cs @@ -0,0 +1,59 @@ +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Command; + +namespace GFramework.Cqrs.Extensions; + +/// +/// 提供对 接口的 CQRS 命令扩展方法。 +/// +/// +/// 该扩展类将命令分发统一路由到架构上下文中的 CQRS 运行时。 +/// +public static class ContextAwareCqrsCommandExtensions +{ + /// + /// 发送命令的同步版本(不推荐,仅用于兼容同步调用链)。 + /// + /// 命令响应类型。 + /// 实现 接口的对象。 + /// 要发送的命令对象。 + /// 命令执行结果。 + /// + /// 当 时抛出。 + /// + /// + /// 同步方法仅用于兼容同步调用链;新代码建议优先使用异步版本。 + /// + public static TResponse SendCommand(this IContextAware contextAware, ICommand command) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendCommand(command); + } + + /// + /// 异步发送命令并返回结果。 + /// + /// 命令响应类型。 + /// 实现 接口的对象。 + /// 要发送的命令对象。 + /// 取消令牌,用于取消操作。 + /// 包含命令执行结果的 + /// + /// 当 时抛出。 + /// + /// + /// 该方法直接返回底层 ,避免额外的 async 状态机分配。 + /// + public static ValueTask SendCommandAsync( + this IContextAware contextAware, + ICommand command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendCommandAsync(command, cancellationToken); + } +} diff --git a/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs new file mode 100644 index 00000000..ef5eb247 --- /dev/null +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsExtensions.cs @@ -0,0 +1,141 @@ +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Extensions; + +/// +/// 提供对 接口的 CQRS 统一扩展方法。 +/// 这些扩展直接委托给架构上下文的内建 CQRS runtime,作为新的中性命名入口。 +/// +public static class ContextAwareCqrsExtensions +{ + /// + /// 发送请求(统一处理 Command/Query)。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 要发送的请求。 + /// 取消令牌。 + /// 请求结果。 + /// + /// 当 时抛出。 + /// + public static ValueTask SendRequestAsync( + this IContextAware contextAware, + IRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(request); + + return contextAware.GetContext().SendRequestAsync(request, cancellationToken); + } + + /// + /// 发送请求(同步版本,不推荐)。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 要发送的请求。 + /// 请求结果。 + /// + /// 当 时抛出。 + /// + public static TResponse SendRequest(this IContextAware contextAware, IRequest request) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(request); + + return contextAware.GetContext().SendRequest(request); + } + + /// + /// 发布通知(一对多事件)。 + /// + /// 通知类型。 + /// 实现 接口的对象。 + /// 要发布的通知。 + /// 取消令牌。 + /// 异步任务。 + /// + /// 当 时抛出。 + /// + public static ValueTask PublishAsync( + this IContextAware contextAware, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(notification); + + return contextAware.GetContext().PublishAsync(notification, cancellationToken); + } + + /// + /// 创建流式请求。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 流式请求。 + /// 取消令牌。 + /// 异步响应流。 + /// + /// 当 时抛出。 + /// + public static IAsyncEnumerable CreateStream( + this IContextAware contextAware, + IStreamRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(request); + + return contextAware.GetContext().CreateStream(request, cancellationToken); + } + + /// + /// 发送无返回值命令。 + /// + /// 命令类型。 + /// 实现 接口的对象。 + /// 要发送的命令。 + /// 取消令牌。 + /// 异步任务。 + /// + /// 当 时抛出。 + /// + public static ValueTask SendAsync( + this IContextAware contextAware, + TCommand command, + CancellationToken cancellationToken = default) + where TCommand : IRequest + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendAsync(command, cancellationToken); + } + + /// + /// 发送带返回值命令。 + /// + /// 响应类型。 + /// 实现 接口的对象。 + /// 要发送的命令。 + /// 取消令牌。 + /// 命令执行结果。 + /// + /// 当 时抛出。 + /// + public static ValueTask SendAsync( + this IContextAware contextAware, + IRequest command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(command); + + return contextAware.GetContext().SendAsync(command, cancellationToken); + } +} diff --git a/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs b/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs new file mode 100644 index 00000000..21c1e952 --- /dev/null +++ b/GFramework.Cqrs/Extensions/ContextAwareCqrsQueryExtensions.cs @@ -0,0 +1,50 @@ +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs.Query; + +namespace GFramework.Cqrs.Extensions; + +/// +/// 提供对 接口的 CQRS 查询扩展方法。 +/// +public static class ContextAwareCqrsQueryExtensions +{ + /// + /// 发送查询的同步版本(不推荐,仅用于兼容同步调用链)。 + /// + /// 查询响应类型。 + /// 实现 接口的对象。 + /// 要发送的查询对象。 + /// 查询结果。 + /// + /// 当 时抛出。 + /// + public static TResponse SendQuery(this IContextAware contextAware, IQuery query) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(query); + + return contextAware.GetContext().SendQuery(query); + } + + /// + /// 异步发送查询并返回结果。 + /// + /// 查询响应类型。 + /// 实现 接口的对象。 + /// 要发送的查询对象。 + /// 取消令牌,用于取消操作。 + /// 包含查询结果的 + /// + /// 当 时抛出。 + /// + public static ValueTask SendQueryAsync( + this IContextAware contextAware, + IQuery query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(contextAware); + ArgumentNullException.ThrowIfNull(query); + + return contextAware.GetContext().SendQueryAsync(query, cancellationToken); + } +} diff --git a/GFramework.Cqrs/GFramework.Cqrs.csproj b/GFramework.Cqrs/GFramework.Cqrs.csproj new file mode 100644 index 00000000..86323d6e --- /dev/null +++ b/GFramework.Cqrs/GFramework.Cqrs.csproj @@ -0,0 +1,17 @@ + + + + GeWuYou.$(AssemblyName) + net8.0;net9.0;net10.0 + disable + enable + true + true + + + + + + + + diff --git a/GFramework.Cqrs/GlobalUsings.cs b/GFramework.Cqrs/GlobalUsings.cs new file mode 100644 index 00000000..3085d1e1 --- /dev/null +++ b/GFramework.Cqrs/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Reflection; +global using Microsoft.Extensions.DependencyInjection; +global using System.Diagnostics; +global using System.Collections.Concurrent; diff --git a/GFramework.Cqrs/ICqrsHandlerRegistry.cs b/GFramework.Cqrs/ICqrsHandlerRegistry.cs new file mode 100644 index 00000000..db3775de --- /dev/null +++ b/GFramework.Cqrs/ICqrsHandlerRegistry.cs @@ -0,0 +1,20 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Cqrs; + +/// +/// 定义由源码生成器产出的 CQRS 处理器注册器契约。 +/// +/// +/// 运行时会优先调用实现该接口的程序集级注册器,以避免在冷启动阶段对整个程序集执行反射扫描。 +/// 当目标程序集没有生成注册器,或生成注册器因兼容性原因不可用时,运行时仍会回退到反射扫描路径。 +/// +public interface ICqrsHandlerRegistry +{ + /// + /// 将当前程序集中的 CQRS 处理器映射注册到目标服务集合。 + /// + /// 承载处理器映射的服务集合。 + /// 用于记录注册诊断信息的日志器。 + void Register(IServiceCollection services, ILogger logger); +} diff --git a/GFramework.Cqrs/ICqrsRegistrationService.cs b/GFramework.Cqrs/ICqrsRegistrationService.cs new file mode 100644 index 00000000..89a0a9d1 --- /dev/null +++ b/GFramework.Cqrs/ICqrsRegistrationService.cs @@ -0,0 +1,19 @@ +using System.Reflection; + +namespace GFramework.Cqrs; + +/// +/// 协调 CQRS 处理器程序集的接入流程。 +/// +/// +/// 该服务封装“程序集去重 + 生成注册器优先 + 反射回退”的默认接入语义, +/// 让 GFramework.Core 的容器层只保留公开入口,而不再直接维护 CQRS handler 注册细节。 +/// +public interface ICqrsRegistrationService +{ + /// + /// 注册一个或多个程序集中的 CQRS 处理器。 + /// + /// 要接入的程序集集合。 + void RegisterHandlers(IEnumerable assemblies); +} diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs new file mode 100644 index 00000000..a6d62f96 --- /dev/null +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -0,0 +1,333 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Abstractions.Rule; +using GFramework.Cqrs.Abstractions.Cqrs; +using ICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; + +namespace GFramework.Cqrs.Internal; + +/// +/// GFramework 自有 CQRS 运行时分发器。 +/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前 CQRS 分发上下文。 +/// +internal sealed class CqrsDispatcher( + IIocContainer container, + ILogger logger) : ICqrsRuntime +{ + // 进程级缓存:缓存通知调用委托,复用并发安全字典以支撑多线程发布路径。 + private static readonly ConcurrentDictionary NotificationInvokers = new(); + + // 进程级缓存:缓存通知处理器服务类型,避免每次发布都重复 MakeGenericType。 + private static readonly ConcurrentDictionary NotificationHandlerServiceTypes = new(); + + // 进程级缓存:缓存流式请求调用委托,避免每次创建流时重复解析反射签名。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = + new(); + + // 进程级缓存:缓存请求处理器与 pipeline 行为的服务类型,减少热路径中的泛型类型构造。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestServiceTypeSet> + RequestServiceTypes = new(); + + // 进程级缓存:缓存流式请求处理器服务类型,避免每次建流时重复 MakeGenericType。 + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), Type> + StreamHandlerServiceTypes = + new(); + + // 静态方法定义缓存:这些反射查找与消息类型无关,只需解析一次即可复用。 + private static readonly MethodInfo RequestHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo RequestPipelineInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo NotificationHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo StreamHandlerInvokerMethodDefinition = typeof(CqrsDispatcher) + .GetMethod(nameof(InvokeStreamHandler), BindingFlags.NonPublic | BindingFlags.Static)!; + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前 CQRS 分发上下文,用于上下文感知处理器注入。 + /// 通知对象。 + /// 取消令牌。 + public async ValueTask PublishAsync( + ICqrsContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(notification); + + var notificationType = notification.GetType(); + var handlerType = NotificationHandlerServiceTypes.GetOrAdd( + notificationType, + static type => typeof(INotificationHandler<>).MakeGenericType(type)); + var handlers = container.GetAll(handlerType); + + if (handlers.Count == 0) + { + logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); + return; + } + + var invoker = NotificationInvokers.GetOrAdd( + notificationType, + CreateNotificationInvoker); + + foreach (var handler in handlers) + { + PrepareHandler(handler, context); + await invoker(handler, notification, cancellationToken); + } + } + + /// + /// 发送请求并返回结果。 + /// + /// 响应类型。 + /// 当前 CQRS 分发上下文,用于上下文感知处理器注入。 + /// 请求对象。 + /// 取消令牌。 + /// 请求响应。 + public async ValueTask SendAsync( + ICqrsContext context, + IRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + var requestType = request.GetType(); + var serviceTypes = RequestServiceTypes.GetOrAdd( + (requestType, typeof(TResponse)), + static key => new RequestServiceTypeSet( + typeof(IRequestHandler<,>).MakeGenericType(key.RequestType, key.ResponseType), + typeof(IPipelineBehavior<,>).MakeGenericType(key.RequestType, key.ResponseType))); + var handlerType = serviceTypes.HandlerType; + var handler = container.Get(handlerType) + ?? throw new InvalidOperationException( + $"No CQRS request handler registered for {requestType.FullName}."); + + PrepareHandler(handler, context); + var behaviors = container.GetAll(serviceTypes.BehaviorType); + + foreach (var behavior in behaviors) + PrepareHandler(behavior, context); + + if (behaviors.Count == 0) + { + var invoker = RequestInvokerCache.Invokers.GetOrAdd( + requestType, + CreateRequestInvoker); + + return await invoker(handler, request, cancellationToken); + } + + var pipelineInvoker = RequestPipelineInvokerCache.Invokers.GetOrAdd( + requestType, + CreateRequestPipelineInvoker); + + return await pipelineInvoker(handler, behaviors, request, cancellationToken); + } + + /// + /// 创建流式请求并返回异步响应序列。 + /// + /// 响应元素类型。 + /// 当前 CQRS 分发上下文,用于上下文感知处理器注入。 + /// 流式请求对象。 + /// 取消令牌。 + /// 异步响应序列。 + public IAsyncEnumerable CreateStream( + ICqrsContext context, + IStreamRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + var requestType = request.GetType(); + var handlerType = StreamHandlerServiceTypes.GetOrAdd( + (requestType, typeof(TResponse)), + static key => typeof(IStreamRequestHandler<,>).MakeGenericType(key.RequestType, key.ResponseType)); + var handler = container.Get(handlerType) + ?? throw new InvalidOperationException( + $"No CQRS stream handler registered for {requestType.FullName}."); + + PrepareHandler(handler, context); + + var invoker = StreamInvokers.GetOrAdd( + (requestType, typeof(TResponse)), + static key => CreateStreamInvoker(key.RequestType, key.ResponseType)); + + return (IAsyncEnumerable)invoker(handler, request, cancellationToken); + } + + /// + /// 为上下文感知处理器注入当前 CQRS 分发上下文。 + /// + /// 处理器实例。 + /// 当前 CQRS 分发上下文。 + private static void PrepareHandler(object handler, ICqrsContext context) + { + if (handler is IContextAware contextAware) + { + if (context is not IArchitectureContext architectureContext) + throw new InvalidOperationException( + "The current CQRS context does not implement IArchitectureContext, so it cannot be injected into IContextAware handlers."); + + contextAware.SetContext(architectureContext); + } + } + + /// + /// 生成请求处理器调用委托,避免每次发送都重复反射。 + /// + private static RequestInvoker CreateRequestInvoker(Type requestType) + { + var method = RequestHandlerInvokerMethodDefinition + .MakeGenericMethod(requestType, typeof(TResponse)); + return (RequestInvoker)Delegate.CreateDelegate(typeof(RequestInvoker), method); + } + + /// + /// 生成带管道行为的请求处理委托,避免每次发送都重复反射。 + /// + private static RequestPipelineInvoker CreateRequestPipelineInvoker(Type requestType) + { + var method = RequestPipelineInvokerMethodDefinition + .MakeGenericMethod(requestType, typeof(TResponse)); + return (RequestPipelineInvoker)Delegate.CreateDelegate( + typeof(RequestPipelineInvoker), + method); + } + + /// + /// 生成通知处理器调用委托,避免每次发布都重复反射。 + /// + private static NotificationInvoker CreateNotificationInvoker(Type notificationType) + { + var method = NotificationHandlerInvokerMethodDefinition + .MakeGenericMethod(notificationType); + return (NotificationInvoker)Delegate.CreateDelegate(typeof(NotificationInvoker), method); + } + + /// + /// 生成流式处理器调用委托,避免每次创建流都重复反射。 + /// + private static StreamInvoker CreateStreamInvoker(Type requestType, Type responseType) + { + var method = StreamHandlerInvokerMethodDefinition + .MakeGenericMethod(requestType, responseType); + return (StreamInvoker)Delegate.CreateDelegate(typeof(StreamInvoker), method); + } + + /// + /// 执行已强类型化的请求处理器调用。 + /// + private static ValueTask InvokeRequestHandlerAsync( + object handler, + object request, + CancellationToken cancellationToken) + where TRequest : IRequest + { + var typedHandler = (IRequestHandler)handler; + var typedRequest = (TRequest)request; + return typedHandler.Handle(typedRequest, cancellationToken); + } + + /// + /// 执行包含管道行为链的请求处理。 + /// + private static ValueTask InvokeRequestPipelineAsync( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken) + where TRequest : IRequest + { + var typedHandler = (IRequestHandler)handler; + var typedRequest = (TRequest)request; + + MessageHandlerDelegate next = + (message, token) => typedHandler.Handle(message, token); + + for (var i = behaviors.Count - 1; i >= 0; i--) + { + var behavior = (IPipelineBehavior)behaviors[i]; + var currentNext = next; + next = (message, token) => behavior.Handle(message, currentNext, token); + } + + return next(typedRequest, cancellationToken); + } + + /// + /// 执行已强类型化的通知处理器调用。 + /// + private static ValueTask InvokeNotificationHandlerAsync( + object handler, + object notification, + CancellationToken cancellationToken) + where TNotification : INotification + { + var typedHandler = (INotificationHandler)handler; + var typedNotification = (TNotification)notification; + return typedHandler.Handle(typedNotification, cancellationToken); + } + + /// + /// 执行已强类型化的流式处理器调用。 + /// + private static object InvokeStreamHandler( + object handler, + object request, + CancellationToken cancellationToken) + where TRequest : IStreamRequest + { + var typedHandler = (IStreamRequestHandler)handler; + var typedRequest = (TRequest)request; + return typedHandler.Handle(typedRequest, cancellationToken); + } + + private delegate ValueTask RequestInvoker( + object handler, + object request, + CancellationToken cancellationToken); + + private delegate ValueTask RequestPipelineInvoker( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken); + + private delegate ValueTask NotificationInvoker(object handler, object notification, + CancellationToken cancellationToken); + + private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + + /// + /// 按响应类型分层缓存 request 处理器调用委托,避免 value-type 响应在 object 桥接中产生装箱。 + /// + /// 请求响应类型。 + private static class RequestInvokerCache + { + internal static readonly ConcurrentDictionary> Invokers = new(); + } + + /// + /// 按响应类型分层缓存带 pipeline 的 request 调用委托,避免 pipeline 热路径上的额外装箱。 + /// + /// 请求响应类型。 + private static class RequestPipelineInvokerCache + { + internal static readonly ConcurrentDictionary> Invokers = new(); + } + + private readonly record struct RequestServiceTypeSet(Type HandlerType, Type BehaviorType); +} diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs new file mode 100644 index 00000000..3604de83 --- /dev/null +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -0,0 +1,393 @@ +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Internal; + +/// +/// 在架构初始化期间扫描并注册 CQRS 处理器。 +/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销; +/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。 +/// +internal static class CqrsHandlerRegistrar +{ + /// + /// 扫描指定程序集并注册所有 CQRS 请求/通知/流式处理器。 + /// + /// 目标容器。 + /// 要扫描的程序集集合。 + /// 日志记录器。 + public static void RegisterHandlers( + IIocContainer container, + IEnumerable assemblies, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(assemblies); + ArgumentNullException.ThrowIfNull(logger); + + foreach (var assembly in assemblies + .Where(static assembly => assembly is not null) + .Distinct() + .OrderBy(GetAssemblySortKey, StringComparer.Ordinal)) + { + var generatedRegistrationResult = + TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger); + if (generatedRegistrationResult is { UsedGeneratedRegistry: true, RequiresReflectionFallback: false }) + continue; + + RegisterAssemblyHandlers( + container.GetServicesUnsafe, + assembly, + logger, + generatedRegistrationResult.ReflectionFallbackMetadata); + } + } + + /// + /// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。 + /// + /// 目标服务集合。 + /// 当前要处理的程序集。 + /// 日志记录器。 + /// 生成注册器的使用结果。 + private static GeneratedRegistrationResult TryRegisterGeneratedHandlers( + IServiceCollection services, + Assembly assembly, + ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + + try + { + var registryTypes = assembly + .GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false) + .OfType() + .Select(static attribute => attribute.RegistryType) + .Where(static type => type is not null) + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + + if (registryTypes.Count == 0) + return GeneratedRegistrationResult.NoGeneratedRegistry(); + + var registries = new List(registryTypes.Count); + foreach (var registryType in registryTypes) + { + if (!typeof(ICqrsHandlerRegistry).IsAssignableFrom(registryType)) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); + return GeneratedRegistrationResult.NoGeneratedRegistry(); + } + + if (registryType.IsAbstract) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); + return GeneratedRegistrationResult.NoGeneratedRegistry(); + } + + if (Activator.CreateInstance(registryType, nonPublic: true) is not ICqrsHandlerRegistry registry) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it could not be instantiated."); + return GeneratedRegistrationResult.NoGeneratedRegistry(); + } + + registries.Add(registry); + } + + foreach (var registry in registries) + { + logger.Debug( + $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); + registry.Register(services, logger); + } + + var reflectionFallbackMetadata = GetReflectionFallbackMetadata(assembly, logger); + if (reflectionFallbackMetadata is not null) + { + if (reflectionFallbackMetadata.HasExplicitTypes) + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s)."); + } + else + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers."); + } + + return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata); + } + + return GeneratedRegistrationResult.FullyHandled(); + } + catch (Exception exception) + { + logger.Warn( + $"Generated CQRS handler registry discovery failed for assembly {assemblyName}. Falling back to reflection scan."); + logger.Warn( + $"Failed to use generated CQRS handler registry for assembly {assemblyName}: {exception.Message}"); + return GeneratedRegistrationResult.NoGeneratedRegistry(); + } + } + + /// + /// 注册单个程序集里的所有 CQRS 处理器映射。 + /// + private static void RegisterAssemblyHandlers( + IServiceCollection services, + Assembly assembly, + ILogger logger, + ReflectionFallbackMetadata? reflectionFallbackMetadata) + { + foreach (var implementationType in GetCandidateHandlerTypes(assembly, logger, reflectionFallbackMetadata) + .Where(IsConcreteHandlerType)) + { + var handlerInterfaces = implementationType + .GetInterfaces() + .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + + if (handlerInterfaces.Count == 0) + continue; + + foreach (var handlerInterface in handlerInterfaces) + { + if (IsHandlerMappingAlreadyRegistered(services, handlerInterface, implementationType)) + { + logger.Debug( + $"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); + continue; + } + + // Request/notification handlers receive context injection before every dispatch. + // Transient registration avoids sharing mutable Context across concurrent requests. + services.AddTransient(handlerInterface, implementationType); + logger.Debug( + $"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); + } + } + } + + /// + /// 根据生成器提供的 fallback 清单或整程序集扫描结果,获取本轮要注册的候选处理器类型。 + /// + private static IReadOnlyList GetCandidateHandlerTypes( + Assembly assembly, + ILogger logger, + ReflectionFallbackMetadata? reflectionFallbackMetadata) + { + return reflectionFallbackMetadata is { HasExplicitTypes: true } + ? reflectionFallbackMetadata.Types + : GetLoadableTypes(assembly, logger); + } + + /// + /// 获取生成注册器要求运行时继续补充反射扫描的 handler 元数据。 + /// + private static ReflectionFallbackMetadata? GetReflectionFallbackMetadata( + Assembly assembly, + ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + var fallbackAttributes = assembly + .GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false) + .OfType() + .ToList(); + + if (fallbackAttributes.Count == 0) + return null; + + var resolvedTypes = new List(); + foreach (var fallbackType in fallbackAttributes + .SelectMany(static attribute => attribute.FallbackHandlerTypes) + .Where(static type => type is not null) + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal)) + { + if (!string.Equals( + GetAssemblySortKey(fallbackType.Assembly), + assemblyName, + StringComparison.Ordinal)) + { + logger.Warn( + $"Generated CQRS reflection fallback type {fallbackType.FullName} was declared on assembly {assemblyName} but belongs to assembly {GetAssemblySortKey(fallbackType.Assembly)}. Skipping mismatched fallback entry."); + continue; + } + + resolvedTypes.Add(fallbackType); + } + + foreach (var typeName in fallbackAttributes + .SelectMany(static attribute => attribute.FallbackHandlerTypeNames) + .Where(static name => !string.IsNullOrWhiteSpace(name)) + .Distinct(StringComparer.Ordinal) + .OrderBy(static name => name, StringComparer.Ordinal)) + { + try + { + var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false); + if (type is null) + { + logger.Warn( + $"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry."); + continue; + } + + resolvedTypes.Add(type); + } + catch (Exception exception) + { + logger.Warn( + $"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}"); + } + } + + return new ReflectionFallbackMetadata( + resolvedTypes + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray()); + } + + /// + /// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。 + /// + private static IReadOnlyList GetLoadableTypes(Assembly assembly, ILogger logger) + { + try + { + return assembly.GetTypes() + .Where(static type => type is not null) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + } + catch (ReflectionTypeLoadException exception) + { + return RecoverLoadableTypes(assembly, exception, logger); + } + } + + /// + /// 记录部分类型加载失败,并返回仍然可用的类型集合。 + /// + private static IReadOnlyList RecoverLoadableTypes( + Assembly assembly, + ReflectionTypeLoadException exception, + ILogger logger) + { + var assemblyName = GetAssemblySortKey(assembly); + logger.Warn( + $"CQRS handler scan partially failed for assembly {assemblyName}. Continuing with loadable types."); + + foreach (var loaderException in exception.LoaderExceptions.Where(static ex => ex is not null)) + { + logger.Warn( + $"Failed to load one or more types while scanning {assemblyName}: {loaderException!.Message}"); + } + + return exception.Types + .Where(static type => type is not null) + .Cast() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToList(); + } + + /// + /// 判断指定类型是否可作为可实例化处理器。 + /// + private static bool IsConcreteHandlerType(Type type) + { + return type is { IsAbstract: false, IsInterface: false } && !type.ContainsGenericParameters; + } + + /// + /// 判断接口是否为当前运行时支持的 CQRS 处理器接口。 + /// + private static bool IsSupportedHandlerInterface(Type type) + { + if (!type.IsGenericType) + return false; + + var definition = type.GetGenericTypeDefinition(); + return definition == typeof(IRequestHandler<,>) || + definition == typeof(INotificationHandler<>) || + definition == typeof(IStreamRequestHandler<,>); + } + + /// + /// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。 + /// + private static bool IsHandlerMappingAlreadyRegistered( + IServiceCollection services, + Type handlerInterface, + Type implementationType) + { + // 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。 + // 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。 + return services.Any(descriptor => + descriptor.ServiceType == handlerInterface && + descriptor.ImplementationType == implementationType); + } + + /// + /// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。 + /// + private static string GetAssemblySortKey(Assembly assembly) + { + return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString(); + } + + /// + /// 生成类型排序键,保证同一程序集内的处理器与接口映射顺序稳定。 + /// + private static string GetTypeSortKey(Type type) + { + return type.FullName ?? type.Name; + } + + private readonly record struct GeneratedRegistrationResult( + bool UsedGeneratedRegistry, + bool RequiresReflectionFallback, + ReflectionFallbackMetadata? ReflectionFallbackMetadata) + { + public static GeneratedRegistrationResult NoGeneratedRegistry() + { + return new GeneratedRegistrationResult( + UsedGeneratedRegistry: false, + RequiresReflectionFallback: false, + ReflectionFallbackMetadata: null); + } + + public static GeneratedRegistrationResult FullyHandled() + { + return new GeneratedRegistrationResult( + UsedGeneratedRegistry: true, + RequiresReflectionFallback: false, + ReflectionFallbackMetadata: null); + } + + public static GeneratedRegistrationResult WithReflectionFallback( + ReflectionFallbackMetadata reflectionFallbackMetadata) + { + ArgumentNullException.ThrowIfNull(reflectionFallbackMetadata); + + return new GeneratedRegistrationResult( + UsedGeneratedRegistry: true, + RequiresReflectionFallback: true, + ReflectionFallbackMetadata: reflectionFallbackMetadata); + } + } + + private sealed class ReflectionFallbackMetadata(IReadOnlyList types) + { + public IReadOnlyList Types { get; } = types ?? throw new ArgumentNullException(nameof(types)); + + public bool HasExplicitTypes => Types.Count > 0; + } +} diff --git a/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs new file mode 100644 index 00000000..70d33562 --- /dev/null +++ b/GFramework.Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Internal; + +/// +/// 默认的 CQRS 处理器注册器实现。 +/// 该适配器把容器公开的 handler 接入入口转发到现有的注册流水线, +/// 使容器主路径只依赖 抽象。 +/// +internal sealed class DefaultCqrsHandlerRegistrar(IIocContainer container, ILogger logger) : ICqrsHandlerRegistrar +{ + private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// 按当前 runtime 约定扫描并注册处理器程序集。 + /// + /// 要接入的程序集集合。 + public void RegisterHandlers(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + CqrsHandlerRegistrar.RegisterHandlers(_container, assemblies, _logger); + } +} diff --git a/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs b/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs new file mode 100644 index 00000000..7993d748 --- /dev/null +++ b/GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs @@ -0,0 +1,62 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Internal; + +/// +/// 默认的 CQRS 程序集注册协调器。 +/// +/// +/// 该实现把“按稳定程序集键去重”和“委托给 handler registrar 执行实际映射注册”收敛到 CQRS runtime 内部, +/// 避免外层容器继续了解 handler 注册流水线的内部结构。 +/// +/// 该类型不是线程安全的。调用方应在外部同步边界内访问 , +/// 例如由容器写锁串行化程序集注册流程。 +/// +/// +internal sealed class DefaultCqrsRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger) + : ICqrsRegistrationService +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly HashSet _registeredAssemblyKeys = new(StringComparer.Ordinal); + private readonly ICqrsHandlerRegistrar _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); + + /// + /// 注册指定程序集中的 CQRS handlers。 + /// + /// 要接入的程序集集合。 + public void RegisterHandlers(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + + var processedAssemblyKeys = new HashSet(StringComparer.Ordinal); + foreach (var assembly in assemblies + .Where(static assembly => assembly is not null) + .OrderBy(GetAssemblyRegistrationKey, StringComparer.Ordinal)) + { + var assemblyKey = GetAssemblyRegistrationKey(assembly); + if (!processedAssemblyKeys.Add(assemblyKey)) + continue; + + if (_registeredAssemblyKeys.Contains(assemblyKey)) + { + _logger.Debug( + $"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered."); + continue; + } + + _registrar.RegisterHandlers([assembly]); + _registeredAssemblyKeys.Add(assemblyKey); + } + } + + /// + /// 生成稳定程序集键,避免相同程序集被不同 实例重复接入。 + /// + /// 目标程序集。 + /// 稳定的程序集标识。 + private static string GetAssemblyRegistrationKey(Assembly assembly) + { + return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString(); + } +} diff --git a/GFramework.Cqrs/Notification/NotificationBase.cs b/GFramework.Cqrs/Notification/NotificationBase.cs new file mode 100644 index 00000000..653a22f2 --- /dev/null +++ b/GFramework.Cqrs/Notification/NotificationBase.cs @@ -0,0 +1,35 @@ +// 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; +using GFramework.Cqrs.Abstractions.Cqrs.Notification; + +namespace GFramework.Cqrs.Notification; + +/// +/// 为携带输入模型的 CQRS 通知提供统一基类。 +/// +/// 通知输入类型,必须实现 +/// 通知广播时携带的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class NotificationBase(TInput input) : INotification + where TInput : INotificationInput +{ + /// + /// 获取通知广播时携带的输入对象。 + /// + public TInput Input => input; +} diff --git a/GFramework.Cqrs/Query/QueryBase.cs b/GFramework.Cqrs/Query/QueryBase.cs new file mode 100644 index 00000000..4da82bf3 --- /dev/null +++ b/GFramework.Cqrs/Query/QueryBase.cs @@ -0,0 +1,35 @@ +// 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.Query; + +namespace GFramework.Cqrs.Query; + +/// +/// 为携带输入模型的 CQRS 查询提供统一基类。 +/// +/// 查询输入类型,必须实现 +/// 查询响应类型。 +/// 查询执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class QueryBase(TInput input) : IQuery + where TInput : IQueryInput +{ + /// + /// 获取查询执行时携带的输入对象。 + /// + public TInput Input => input; +} diff --git a/GFramework.Cqrs/Request/RequestBase.cs b/GFramework.Cqrs/Request/RequestBase.cs new file mode 100644 index 00000000..08ceabe4 --- /dev/null +++ b/GFramework.Cqrs/Request/RequestBase.cs @@ -0,0 +1,36 @@ +// 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; +using GFramework.Cqrs.Abstractions.Cqrs.Request; + +namespace GFramework.Cqrs.Request; + +/// +/// 为携带输入模型的通用 CQRS 请求提供统一基类。 +/// +/// 请求输入类型,必须实现 +/// 请求响应类型。 +/// 请求执行所需的输入对象。 +/// +/// 该类型继续保留在历史公开命名空间中,以避免调用方因 runtime 程序集拆分而批量修改继承层次。 +/// 具体实现现由 GFramework.Cqrs 程序集承载,并通过 type forward 维持旧程序集兼容性。 +/// +public abstract class RequestBase(TInput input) : IRequest + where TInput : IRequestInput +{ + /// + /// 获取请求执行时携带的输入对象。 + /// + public TInput Input => input; +} diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs index 1c2699ae..71d523eb 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记类型允许为带映射特性的导出集合生成批量注册代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs index 3b73af7b..4e56cd9b 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记场景根节点类型,Source Generator 会生成场景行为样板代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs index 56957fb8..0356c43c 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 标记 UI 页面类型,Source Generator 会生成页面行为样板代码。 diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs index dd809fc4..c4fe14a2 100644 --- a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs +++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs @@ -1,4 +1,4 @@ -namespace GFramework.Godot.SourceGenerators.Abstractions; +namespace GFramework.Godot.SourceGenerators.Abstractions.UI; /// /// 声明导出集合应当转发到哪个注册器成员及其方法。 diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index b50b0d73..2c6dc972 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -11,10 +11,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -88,10 +88,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -137,10 +137,10 @@ public class AutoSceneGeneratorTests const string source = """ #nullable enable using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -225,10 +225,10 @@ public class AutoSceneGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute @@ -279,10 +279,10 @@ public class AutoSceneGeneratorTests const string source = """ using System; using GFramework.Game.Abstractions.Scene; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoSceneAttribute : Attribute diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs index 8e7d1115..2b8aca67 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs @@ -11,10 +11,10 @@ public class AutoUiPageGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute @@ -100,10 +100,10 @@ public class AutoUiPageGeneratorTests { const string source = """ using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute @@ -183,10 +183,10 @@ public class AutoUiPageGeneratorTests const string source = """ #nullable enable using System; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoUiPageAttribute : Attribute diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 3a67d3c5..befdcf59 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -13,9 +13,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -86,9 +86,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -141,9 +141,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -207,9 +207,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -284,9 +284,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -344,9 +344,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -414,9 +414,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -482,9 +482,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -549,9 +549,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -604,9 +604,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -659,9 +659,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests const string source = """ using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } @@ -715,9 +715,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests #nullable enable using System; using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions; + using GFramework.Godot.SourceGenerators.Abstractions.UI; - namespace GFramework.Godot.SourceGenerators.Abstractions + namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs index f2920be4..ee389f0e 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs @@ -18,7 +18,8 @@ namespace GFramework.Godot.SourceGenerators.Behavior; public sealed class AutoSceneGenerator : IIncrementalGenerator { private const string AutoSceneAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoSceneAttribute"; + private static readonly string[] GeneratedMemberNames = [ "SceneKeyStr", diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs index 1e0fe7f6..ca193956 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs @@ -12,7 +12,7 @@ namespace GFramework.Godot.SourceGenerators.Behavior; public sealed class AutoUiPageGenerator : IIncrementalGenerator { private const string AutoUiPageAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoUiPageAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoUiPageAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { diff --git a/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets b/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets index 5116e4da..fdcf958e 100644 --- a/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets +++ b/GFramework.Godot.SourceGenerators/GeWuYou.GFramework.Godot.SourceGenerators.targets @@ -18,9 +18,17 @@ - - - + + + + diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index 12589767..93219dcb 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -18,10 +18,10 @@ namespace GFramework.Godot.SourceGenerators.Registration; public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator { private const string AutoRegisterExportedCollectionsAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoRegisterExportedCollectionsAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoRegisterExportedCollectionsAttribute"; private const string RegisterExportedCollectionAttributeMetadataName = - $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.RegisterExportedCollectionAttribute"; + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.RegisterExportedCollectionAttribute"; private const string GeneratedMethodName = "__RegisterExportedCollections_Generated"; diff --git a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs index f0ccc21b..15ee4561 100644 --- a/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs +++ b/GFramework.Godot/Coroutine/ContextAwareCoroutineExtensions.cs @@ -1,8 +1,10 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Extensions; -using GFramework.Core.Extensions; -using Mediator; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Query; +using GFramework.Cqrs.Extensions; namespace GFramework.Godot.Coroutine; @@ -27,8 +29,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .SendCommandAsync(command, cancellationToken) + return ContextAwareCqrsCommandExtensions + .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); @@ -51,8 +53,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .SendCommandAsync(command, cancellationToken) + return ContextAwareCqrsCommandExtensions + .SendCommandAsync(contextAware, command, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); @@ -75,8 +77,8 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .SendQueryAsync(query, cancellationToken) + return ContextAwareCqrsQueryExtensions + .SendQueryAsync(contextAware, query, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); @@ -98,10 +100,10 @@ public static class ContextAwareCoroutineExtensions string? tag = null, CancellationToken cancellationToken = default) { - return contextAware - .PublishAsync(notification, cancellationToken) + return ContextAwareCqrsExtensions + .PublishAsync(contextAware, notification, cancellationToken) .AsTask() .ToCoroutineEnumerator() .RunCoroutine(segment, tag); } -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Common/Constants/PathContests.cs b/GFramework.SourceGenerators.Common/Constants/PathContests.cs index 3facd8b6..a9416fbe 100644 --- a/GFramework.SourceGenerators.Common/Constants/PathContests.cs +++ b/GFramework.SourceGenerators.Common/Constants/PathContests.cs @@ -15,6 +15,11 @@ public static class PathContests /// public const string CoreNamespace = $"{BaseNamespace}.Core"; + /// + /// GFramework CQRS runtime 命名空间 + /// + public const string CqrsNamespace = $"{BaseNamespace}.Cqrs"; + /// /// GFramework Godot模块命名空间 /// @@ -45,4 +50,9 @@ public static class PathContests /// GFramework核心抽象层命名空间 /// public const string CoreAbstractionsNamespace = $"{CoreNamespace}.Abstractions"; -} \ No newline at end of file + + /// + /// GFramework CQRS 抽象层命名空间 + /// + public const string CqrsAbstractionsNamespace = $"{CqrsNamespace}.Abstractions"; +} diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs index a622d387..bed493e7 100644 --- a/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs @@ -16,6 +16,24 @@ public static class GeneratorTest public static async Task RunAsync( string source, params (string filename, string content)[] generatedSources) + { + await RunAsync( + source, + additionalReferences: [], + generatedSources); + } + + /// + /// 运行源代码生成器测试,并为测试编译显式追加元数据引用。 + /// + /// 输入的源代码。 + /// 附加元数据引用,用于构造多程序集场景。 + /// 期望生成的源文件集合,包含文件名和内容的元组。 + /// 异步操作任务。 + public static async Task RunAsync( + string source, + IEnumerable additionalReferences, + params (string filename, string content)[] generatedSources) { var test = new CSharpSourceGeneratorTest { @@ -31,6 +49,9 @@ public static class GeneratorTest test.TestState.GeneratedSources.Add( (typeof(TGenerator), filename, NormalizeLineEndings(content))); + foreach (var additionalReference in additionalReferences) + test.TestState.AdditionalReferences.Add(additionalReference); + await test.RunAsync(); } @@ -46,4 +67,4 @@ public static class GeneratorTest .Replace("\r", "\n", StringComparison.Ordinal) .Replace("\n", Environment.NewLine, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs b/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs new file mode 100644 index 00000000..45fd506a --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Core/MetadataReferenceTestBuilder.cs @@ -0,0 +1,74 @@ +using System.Collections.Immutable; +using System.IO; + +namespace GFramework.SourceGenerators.Tests.Core; + +/// +/// 为多程序集源生成器测试构建内存元数据引用。 +/// +public static class MetadataReferenceTestBuilder +{ + // Reuse the runtime reference set across generator tests to avoid reparsing TRUSTED_PLATFORM_ASSEMBLIES + // for every in-memory compilation. + private static readonly Lazy> CachedRuntimeReferences = + new(CreateRuntimeMetadataReferences); + + /// + /// 将给定源码编译为内存程序集,并返回可供测试编译消费的元数据引用。 + /// + /// 目标程序集名称。 + /// 待编译源码。 + /// 附加元数据引用,用于构造依赖链。 + /// 编译成功后的内存元数据引用。 + public static MetadataReference CreateFromSource( + string assemblyName, + string source, + params MetadataReference[] additionalReferences) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = CachedRuntimeReferences.Value + .Concat(additionalReferences) + .ToImmutableArray(); + var compilation = CSharpCompilation.Create( + assemblyName, + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using var stream = new MemoryStream(); + var emitResult = compilation.Emit(stream); + if (!emitResult.Success) + { + var diagnostics = string.Join( + Environment.NewLine, + emitResult.Diagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .Select(static diagnostic => diagnostic.ToString())); + throw new InvalidOperationException( + $"Failed to build metadata reference '{assemblyName}'.{Environment.NewLine}{diagnostics}"); + } + + stream.Position = 0; + return MetadataReference.CreateFromImage(stream.ToArray()); + } + + /// + /// 获取当前测试运行时可直接复用的基础元数据引用集合。 + /// + /// 当前运行时可信平台程序集对应的元数据引用。 + public static ImmutableArray GetRuntimeMetadataReferences() + { + return CachedRuntimeReferences.Value; + } + + private static ImmutableArray CreateRuntimeMetadataReferences() + { + var trustedPlatformAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))? + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + ?? Array.Empty(); + + return trustedPlatformAssemblies + .Select(static path => (MetadataReference)MetadataReference.CreateFromFile(path)) + .ToImmutableArray(); + } +} diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs new file mode 100644 index 00000000..2e321993 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -0,0 +1,1173 @@ +using System.Reflection; +using GFramework.SourceGenerators.Cqrs; +using GFramework.SourceGenerators.Tests.Core; + +namespace GFramework.SourceGenerators.Tests.Cqrs; + +/// +/// 验证 CQRS 处理器注册生成器的输出与回退边界。 +/// +[TestFixture] +public class CqrsHandlerRegistryGeneratorTests +{ + private const string HiddenNestedHandlerSelfRegistrationExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, typeof(string)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + typeof(global::TestApp.VisibleHandler)); + logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + + """; + + private const string HiddenImplementationDirectInterfaceRegistrationExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + + """; + + private const string HiddenArrayResponseFallbackExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1Element = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1Element is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType()); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + } + + """; + + private const string HiddenGenericEnvelopeResponseExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1GenericDefinition = registryAssembly.GetType("TestApp.Container+HiddenEnvelope`1", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1GenericDefinition is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1GenericDefinition.MakeGenericType(typeof(string))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler>."); + } + } + } + } + + """; + + private const string MixedDirectAndPreciseRegistrationsExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = typeof(global::TestApp.Container.MixedHandler); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1Element = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1Element is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType()); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.MixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.MixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + + """; + + private const string MixedReflectedImplementationAndPreciseRegistrationsExpected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly; + + var implementationType0 = registryAssembly.GetType("TestApp.Container+HiddenMixedHandler", throwOnError: false, ignoreCase: false); + if (implementationType0 is not null) + { + var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false); + var serviceType0_0Argument1Element = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false); + if (serviceType0_0Argument0 is not null && serviceType0_0Argument1Element is not null) + { + var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1Element.MakeArrayType()); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + serviceType0_0, + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenMixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + implementationType0); + logger.Debug("Registered CQRS handler TestApp.Container.HiddenMixedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + } + } + } + + """; + + /// + /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 + /// + [Test] + public async Task Generates_Assembly_Level_Cqrs_Handler_Registry() + { + const string source = """ + using System; + using System.Collections.Generic; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record PingQuery() : IRequest; + public sealed record DomainEvent() : INotification; + public sealed record NumberStream() : IStreamRequest; + + public sealed class ZetaNotificationHandler : INotificationHandler { } + public sealed class AlphaQueryHandler : IRequestHandler { } + public sealed class StreamHandler : IStreamRequestHandler { } + } + """; + + const string expected = """ + // + #nullable enable + + [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))] + + namespace GFramework.Generated.Cqrs; + + internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry + { + public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger) + { + if (services is null) + throw new global::System.ArgumentNullException(nameof(services)); + if (logger is null) + throw new global::System.ArgumentNullException(nameof(logger)); + + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), + typeof(global::TestApp.AlphaQueryHandler)); + logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler."); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), + typeof(global::TestApp.StreamHandler)); + logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler."); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient( + services, + typeof(global::GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler), + typeof(global::TestApp.ZetaNotificationHandler)); + logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler."); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", expected)); + } + + /// + /// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册, + /// 不再依赖程序集级 fallback marker。 + /// + [Test] + public async Task + Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_Handler() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); + } + + /// + /// 验证当隐藏实现类型的 handler 接口仍可被生成代码直接引用时, + /// 生成器只会定向反射实现类型,而不会再生成基于 GetInterfaces() 的接口发现辅助逻辑。 + /// + [Test] + public async Task + Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenImplementationDirectInterfaceRegistrationExpected)); + } + + /// + /// 验证精确重建路径会递归覆盖隐藏元素类型数组, + /// 使这类 handler interface 也能直接生成 closed service type,而不再退回 GetInterfaces()。 + /// + [Test] + public async Task Generates_Precise_Service_Type_For_Hidden_Array_Type_Arguments() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenArrayResponseFallbackExpected)); + } + + /// + /// 验证精确重建路径会递归覆盖隐藏泛型定义, + /// 使“隐藏泛型定义 + 可见/常量型实参”的闭包类型也能直接生成 closed service type。 + /// + [Test] + public async Task Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private sealed class HiddenEnvelope { } + + private sealed record HiddenRequest() : IRequest>; + + private sealed class HiddenHandler : IRequestHandler> { } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected)); + } + + /// + /// 验证同一个 implementation 同时包含可直接注册接口与需精确重建接口时, + /// 生成器会保留两类注册,并继续按 handler interface 名称稳定排序。 + /// + [Test] + public async Task Generates_Mixed_Direct_And_Precise_Registrations_For_Same_Implementation() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + public sealed class MixedHandler : + IRequestHandler, + IRequestHandler + { + } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", MixedDirectAndPreciseRegistrationsExpected)); + } + + /// + /// 验证隐藏 implementation 同时包含可见 handler interface 与需精确重建接口时, + /// 生成器会保留两类注册,而不会让可见接口被整实现回退吞掉。 + /// + [Test] + public async Task Generates_Mixed_Reflected_Implementation_And_Precise_Registrations_For_Same_Implementation() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenMixedHandler : + IRequestHandler, + IRequestHandler + { + } + } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", MixedReflectedImplementationAndPreciseRegistrationsExpected)); + } + + /// + /// 验证当外部基类暴露的 handler interface 含有生成注册器顶层上下文不可直接引用的 protected 类型时, + /// 生成器会保留已知直注册,并只对剩余未知接口做本地 interface discovery。 + /// + [Test] + public void Generates_Partial_Runtime_Interface_Discovery_For_Inaccessible_External_Protected_Types() + { + const string contractsSource = """ + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + """; + + const string dependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public sealed record VisibleRequest() : IRequest; + + public abstract class VisibilityScope + { + protected internal sealed record ProtectedResponse(); + + protected internal sealed record ProtectedRequest() : IRequest; + } + + public abstract class HandlerBase : + VisibilityScope, + IRequestHandler, + IRequestHandler + { + } + """; + + const string source = """ + using System; + using Dep; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + public sealed class DerivedHandler : HandlerBase + { + } + } + """; + + var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( + "Contracts", + contractsSource); + var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( + "Dependency", + dependencySource, + contractsReference); + var generatedSource = RunGenerator( + source, + contractsReference, + dependencyReference); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain("var implementationType0 = typeof(global::TestApp.DerivedHandler);")); + Assert.That( + generatedSource, + Does.Contain( + "var knownServiceTypes0 = new global::System.Collections.Generic.HashSet();")); + Assert.That( + generatedSource, + Does.Contain( + "// Remaining runtime interface discovery target: GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler")); + Assert.That( + generatedSource, + Does.Contain( + "knownServiceTypes0.Add(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler));")); + Assert.That( + generatedSource, + Does.Contain( + "RegisterRemainingReflectedHandlerInterfaces(services, logger, implementationType0, knownServiceTypes0);")); + Assert.That( + generatedSource, + Does.Contain("if (knownServiceTypes.Contains(handlerInterface))")); + Assert.That( + generatedSource, + Does.Contain( + "Registered CQRS handler TestApp.DerivedHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler.")); + Assert.That( + generatedSource, + Does.Not.Contain( + "typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler + /// 验证即使 runtime 仍暴露旧版无参 fallback marker,生成器也会优先在生成注册器内部处理隐藏 handler, + /// 不再输出 fallback marker。 + /// + [Test] + public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_Handler() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute() { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); + } + + /// + /// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性, + /// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。 + /// + [Test] + public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + await GeneratorTest.RunAsync( + source, + ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); + } + + /// + /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 + /// + [Test] + public void Escape_String_Literal_Handles_Control_Characters() + { + var method = typeof(CqrsHandlerRegistryGenerator).GetMethod( + "EscapeStringLiteral", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(method, Is.Not.Null); + + const string input = "line1\r\nline2\\\""; + const string expected = "line1\\r\\nline2\\\\\\\""; + var escaped = method!.Invoke(null, [input]) as string; + + Assert.That(escaped, Is.EqualTo(expected)); + } + + /// + /// 运行 CQRS handler registry generator,并返回单个生成文件的源码文本。 + /// + private static string RunGenerator( + string source, + params MetadataReference[] additionalReferences) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create( + "TestProject", + [syntaxTree], + MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()], + parseOptions: (CSharpParseOptions)syntaxTree.Options); + driver = driver.RunGeneratorsAndUpdateCompilation( + compilation, + out var updatedCompilation, + out _); + + var compilationErrors = updatedCompilation.GetDiagnostics() + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.That( + compilationErrors, + Is.Empty, + () => + $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); + + var runResult = driver.GetRunResult(); + Assert.That(runResult.Results, Has.Length.EqualTo(1)); + Assert.That(runResult.Results[0].GeneratedSources, Has.Length.EqualTo(1)); + + return runResult.Results[0].GeneratedSources[0].SourceText.ToString(); + } +} diff --git a/GFramework.SourceGenerators.Tests/GlobalUsings.cs b/GFramework.SourceGenerators.Tests/GlobalUsings.cs index 78c09fee..03266a66 100644 --- a/GFramework.SourceGenerators.Tests/GlobalUsings.cs +++ b/GFramework.SourceGenerators.Tests/GlobalUsings.cs @@ -20,4 +20,5 @@ global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.Text; global using Microsoft.CodeAnalysis.CSharp.Testing; global using Microsoft.CodeAnalysis.Testing; -global using NUnit.Framework; \ No newline at end of file +global using Microsoft.CodeAnalysis.CSharp; +global using NUnit.Framework; diff --git a/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs new file mode 100644 index 00000000..72ce6664 --- /dev/null +++ b/GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -0,0 +1,1239 @@ +using GFramework.SourceGenerators.Common.Constants; + +namespace GFramework.SourceGenerators.Cqrs; + +/// +/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 +/// +[Generator] +public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator +{ + private const string CqrsContractsNamespace = $"{PathContests.CqrsAbstractionsNamespace}.Cqrs"; + private const string CqrsRuntimeNamespace = PathContests.CqrsNamespace; + private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging"; + private const string IRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IRequestHandler`2"; + private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1"; + private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2"; + private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry"; + + private const string CqrsHandlerRegistryAttributeMetadataName = + $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; + + private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger"; + private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; + private const string GeneratedNamespace = "GFramework.Generated.Cqrs"; + private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry"; + private const string HintName = "CqrsHandlerRegistry.g.cs"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var generationEnvironment = context.CompilationProvider + .Select(static (compilation, _) => CreateGenerationEnvironment(compilation)); + + // Restrict semantic analysis to type declarations that can actually contribute implemented interfaces. + var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => IsHandlerCandidate(node), + static (syntaxContext, _) => TransformHandlerCandidate(syntaxContext)) + .Where(static candidate => candidate is not null) + .Collect(); + + context.RegisterSourceOutput( + generationEnvironment.Combine(handlerCandidates), + static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right)); + } + + private static GenerationEnvironment CreateGenerationEnvironment(Compilation compilation) + { + var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null && + compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null && + compilation.GetTypeByMetadataName( + CqrsHandlerRegistryAttributeMetadataName) is not null && + compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && + compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; + + return new GenerationEnvironment(generationEnabled); + } + + private static bool IsHandlerCandidate(SyntaxNode node) + { + return node is TypeDeclarationSyntax + { + BaseList.Types.Count: > 0 + }; + } + + private static HandlerCandidateAnalysis? TransformHandlerCandidate(GeneratorSyntaxContext context) + { + if (context.Node is not TypeDeclarationSyntax typeDeclaration) + return null; + + if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol type) + return null; + + if (!IsConcreteHandlerType(type)) + return null; + + var handlerInterfaces = type.AllInterfaces + .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToImmutableArray(); + + if (handlerInterfaces.IsDefaultOrEmpty) + return null; + + var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var implementationLogName = GetLogDisplayName(type); + var canReferenceImplementation = CanReferenceFromGeneratedRegistry(context.SemanticModel.Compilation, type); + var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var reflectedImplementationRegistrations = + ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var preciseReflectedRegistrations = + ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var runtimeDiscoveredHandlerInterfaceLogNames = + ImmutableArray.CreateBuilder(handlerInterfaces.Length); + var requiresRuntimeInterfaceDiscovery = false; + foreach (var handlerInterface in handlerInterfaces) + { + var canReferenceHandlerInterface = + CanReferenceFromGeneratedRegistry(context.SemanticModel.Compilation, handlerInterface); + if (canReferenceImplementation && canReferenceHandlerInterface) + { + registrations.Add(new HandlerRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + implementationTypeDisplayName, + GetLogDisplayName(handlerInterface), + implementationLogName)); + continue; + } + + if (!canReferenceImplementation && canReferenceHandlerInterface) + { + reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + GetLogDisplayName(handlerInterface))); + continue; + } + + if (TryCreatePreciseReflectedRegistration( + context.SemanticModel.Compilation, + handlerInterface, + out var preciseReflectedRegistration)) + { + preciseReflectedRegistrations.Add(preciseReflectedRegistration); + continue; + } + + // 某些关闭 handler interface 仍包含只能在实现类型运行时语义里解析的类型形态。 + // 对这些边角场景保留“已知接口静态注册 + 剩余接口运行时补洞”的组合路径, + // 避免单个未知接口把同实现上的其它已知注册全部拖回整实现反射发现。 + requiresRuntimeInterfaceDiscovery = true; + runtimeDiscoveredHandlerInterfaceLogNames.Add(GetLogDisplayName(handlerInterface)); + } + + return new HandlerCandidateAnalysis( + implementationTypeDisplayName, + implementationLogName, + registrations.ToImmutable(), + reflectedImplementationRegistrations.ToImmutable(), + preciseReflectedRegistrations.ToImmutable(), + canReferenceImplementation ? null : GetReflectionTypeMetadataName(type), + requiresRuntimeInterfaceDiscovery, + runtimeDiscoveredHandlerInterfaceLogNames.ToImmutable()); + } + + private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, + ImmutableArray candidates) + { + if (!generationEnvironment.GenerationEnabled) + return; + + var registrations = CollectRegistrations(candidates); + + if (registrations.Count == 0) + return; + + context.AddSource( + HintName, + GenerateSource(registrations)); + } + + private static List CollectRegistrations( + ImmutableArray candidates) + { + var registrations = new List(); + + // Partial declarations surface the same symbol through multiple syntax nodes. + // Collapse them by implementation type so direct and reflected registrations stay stable and duplicate-free. + var uniqueCandidates = new Dictionary(StringComparer.Ordinal); + + foreach (var candidate in candidates) + { + if (candidate is null) + continue; + + uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value; + } + + foreach (var candidate in uniqueCandidates.Values) + { + registrations.Add(new ImplementationRegistrationSpec( + candidate.ImplementationTypeDisplayName, + candidate.ImplementationLogName, + candidate.Registrations, + candidate.ReflectedImplementationRegistrations, + candidate.PreciseReflectedRegistrations, + candidate.ReflectionTypeMetadataName, + candidate.RequiresRuntimeInterfaceDiscovery, + candidate.RuntimeDiscoveredHandlerInterfaceLogNames)); + } + + registrations.Sort(static (left, right) => + { + var implementationComparison = StringComparer.Ordinal.Compare( + left.ImplementationLogName, + right.ImplementationLogName); + + return implementationComparison; + }); + + return registrations; + } + + private static bool IsConcreteHandlerType(INamedTypeSymbol type) + { + return type.TypeKind is TypeKind.Class or TypeKind.Struct && + !type.IsAbstract && + !ContainsGenericParameters(type); + } + + private static bool ContainsGenericParameters(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.ContainingType) + { + if (current.TypeParameters.Length > 0) + return true; + } + + return false; + } + + private static bool IsSupportedHandlerInterface(INamedTypeSymbol interfaceType) + { + if (!interfaceType.IsGenericType) + return false; + + var definitionMetadataName = GetFullyQualifiedMetadataName(interfaceType.OriginalDefinition); + return string.Equals(definitionMetadataName, IRequestHandlerMetadataName, StringComparison.Ordinal) || + string.Equals(definitionMetadataName, INotificationHandlerMetadataName, StringComparison.Ordinal) || + string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal); + } + + /// + /// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。 + /// + /// + /// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。 + /// + /// + /// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。 + /// + /// + /// 当方法返回 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述; + /// 当方法返回 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。 + /// + /// + /// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 ; + /// 只要任一泛型实参无法安全编码到生成输出中,就返回 。 + /// + private static bool TryCreatePreciseReflectedRegistration( + Compilation compilation, + INamedTypeSymbol handlerInterface, + out PreciseReflectedRegistrationSpec registration) + { + var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition + .ConstructUnboundGenericType() + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var typeArguments = + ImmutableArray.CreateBuilder(handlerInterface.TypeArguments.Length); + foreach (var typeArgument in handlerInterface.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference)) + { + registration = default; + return false; + } + + typeArguments.Add(runtimeTypeReference!); + } + + registration = new PreciseReflectedRegistrationSpec( + openHandlerTypeDisplayName, + GetLogDisplayName(handlerInterface), + typeArguments.ToImmutable()); + return true; + } + + /// + /// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。 + /// + /// + /// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。 + /// + /// + /// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。 + /// + /// + /// 当方法返回 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示; + /// 当方法返回 时为 ,调用方应回退到更宽泛的实现类型反射扫描策略。 + /// + /// + /// 当 及其递归子结构都能映射为稳定的运行时引用时返回 ; + /// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 。 + /// + private static bool TryCreateRuntimeTypeReference( + Compilation compilation, + ITypeSymbol type, + out RuntimeTypeReferenceSpec? runtimeTypeReference) + { + if (CanReferenceFromGeneratedRegistry(compilation, type)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( + type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + + if (type is IArrayTypeSymbol arrayType && + TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank); + return true; + } + + if (type is INamedTypeSymbol genericNamedType && + genericNamedType.IsGenericType && + !genericNamedType.IsUnboundGenericType && + TryCreateGenericTypeDefinitionReference(compilation, genericNamedType, + out var genericTypeDefinitionReference)) + { + var genericTypeArguments = + ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); + foreach (var typeArgument in genericNamedType.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference)) + { + runtimeTypeReference = null; + return false; + } + + genericTypeArguments.Add(genericTypeArgumentReference!); + } + + runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric( + genericTypeDefinitionReference!, + genericTypeArguments.ToImmutable()); + return true; + } + + if (type is INamedTypeSymbol namedType && + SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup( + GetReflectionTypeMetadataName(namedType)); + return true; + } + + runtimeTypeReference = null; + return false; + } + + /// + /// 为已构造泛型类型解析其泛型定义的运行时引用描述。 + /// + /// + /// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。 + /// + /// + /// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。 + /// + /// + /// 当方法返回 时,包含泛型定义的直接引用或反射查找描述; + /// 当方法返回 时为 ,调用方应停止精确构造并回退到更保守的注册路径。 + /// + /// + /// 当泛型定义能够以稳定方式编码到生成输出中时返回 ; + /// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 。 + /// + private static bool TryCreateGenericTypeDefinitionReference( + Compilation compilation, + INamedTypeSymbol genericNamedType, + out RuntimeTypeReferenceSpec? genericTypeDefinitionReference) + { + var genericTypeDefinition = genericNamedType.OriginalDefinition; + if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition)) + { + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference( + genericTypeDefinition + .ConstructUnboundGenericType() + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + + if (SymbolEqualityComparer.Default.Equals(genericTypeDefinition.ContainingAssembly, compilation.Assembly)) + { + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromReflectionLookup( + GetReflectionTypeMetadataName(genericTypeDefinition)); + return true; + } + + genericTypeDefinitionReference = null; + return false; + } + + private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type) + { + switch (type) + { + case IArrayTypeSymbol arrayType: + return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType); + case INamedTypeSymbol namedType: + if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null)) + return false; + + foreach (var typeArgument in namedType.TypeArguments) + { + if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument)) + return false; + } + + return true; + case IPointerTypeSymbol pointerType: + return CanReferenceFromGeneratedRegistry(compilation, pointerType.PointedAtType); + case ITypeParameterSymbol: + return false; + default: + // Treat other Roslyn type kinds, such as dynamic or unresolved error types, as referenceable for now. + // If a real-world case proves unsafe, tighten this branch instead of broadening the named-type path above. + return true; + } + } + + private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type) + { + var nestedTypes = new Stack(); + for (var current = type; current is not null; current = current.ContainingType) + { + nestedTypes.Push(current.MetadataName); + } + + var builder = new StringBuilder(); + if (!type.ContainingNamespace.IsGlobalNamespace) + { + builder.Append(type.ContainingNamespace.ToDisplayString()); + builder.Append('.'); + } + + while (nestedTypes.Count > 0) + { + builder.Append(nestedTypes.Pop()); + if (nestedTypes.Count > 0) + builder.Append('.'); + } + + return builder.ToString(); + } + + private static string GetReflectionTypeMetadataName(INamedTypeSymbol type) + { + var nestedTypes = new Stack(); + for (var current = type; current is not null; current = current.ContainingType) + { + nestedTypes.Push(current.MetadataName); + } + + var builder = new StringBuilder(); + if (!type.ContainingNamespace.IsGlobalNamespace) + { + builder.Append(type.ContainingNamespace.ToDisplayString()); + builder.Append('.'); + } + + var isFirstType = true; + while (nestedTypes.Count > 0) + { + if (!isFirstType) + builder.Append('+'); + + builder.Append(nestedTypes.Pop()); + isFirstType = false; + } + + return builder.ToString(); + } + + private static string GetTypeSortKey(ITypeSymbol type) + { + return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + private static string GetLogDisplayName(ITypeSymbol type) + { + return GetTypeSortKey(type).Replace("global::", string.Empty); + } + + private static string GenerateSource( + IReadOnlyList registrations) + { + var hasReflectedImplementationRegistrations = registrations.Any(static registration => + !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); + var hasPreciseReflectedRegistrations = registrations.Any(static registration => + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); + var hasRuntimeInterfaceDiscovery = registrations.Any(static registration => + registration.RequiresRuntimeInterfaceDiscovery); + var builder = new StringBuilder(); + builder.AppendLine("// "); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); + builder.Append(GeneratedNamespace); + builder.Append('.'); + builder.Append(GeneratedTypeName); + builder.AppendLine("))]"); + + builder.AppendLine(); + builder.Append("namespace "); + builder.Append(GeneratedNamespace); + builder.AppendLine(";"); + builder.AppendLine(); + builder.Append("internal sealed class "); + builder.Append(GeneratedTypeName); + builder.Append(" : global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".ICqrsHandlerRegistry"); + builder.AppendLine("{"); + builder.Append( + " public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::"); + builder.Append(LoggingNamespace); + builder.AppendLine(".ILogger logger)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (services is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); + builder.AppendLine(" if (logger is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); + if (hasReflectedImplementationRegistrations || hasPreciseReflectedRegistrations || + registrations.Any(static registration => + !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName))) + { + builder.AppendLine(); + builder.Append(" var registryAssembly = typeof(global::"); + builder.Append(GeneratedNamespace); + builder.Append('.'); + builder.Append(GeneratedTypeName); + builder.AppendLine(").Assembly;"); + } + + if (registrations.Count > 0) + builder.AppendLine(); + + for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++) + { + var registration = registrations[registrationIndex]; + if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty || + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty || + registration.RequiresRuntimeInterfaceDiscovery) + { + AppendOrderedImplementationRegistrations(builder, registration, registrationIndex); + } + else if (!registration.DirectRegistrations.IsDefaultOrEmpty) + { + AppendDirectRegistrations(builder, registration); + } + } + + builder.AppendLine(" }"); + + if (hasRuntimeInterfaceDiscovery) + { + builder.AppendLine(); + AppendReflectionHelpers(builder); + } + + builder.AppendLine("}"); + return builder.ToString(); + } + + private static void AppendDirectRegistrations( + StringBuilder builder, + ImplementationRegistrationSpec registration) + { + foreach (var directRegistration in registration.DirectRegistrations) + { + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" typeof("); + builder.Append(directRegistration.ImplementationTypeDisplayName); + builder.AppendLine("));"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(directRegistration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + } + } + + private static void AppendOrderedImplementationRegistrations( + StringBuilder builder, + ImplementationRegistrationSpec registration, + int registrationIndex) + { + var orderedRegistrations = + new List<(string HandlerInterfaceLogName, OrderedRegistrationKind Kind, int Index)>( + registration.DirectRegistrations.Length + + registration.ReflectedImplementationRegistrations.Length + + registration.PreciseReflectedRegistrations.Length); + + for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++) + { + orderedRegistrations.Add(( + registration.DirectRegistrations[directIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.Direct, + directIndex)); + } + + for (var reflectedIndex = 0; + reflectedIndex < registration.ReflectedImplementationRegistrations.Length; + reflectedIndex++) + { + orderedRegistrations.Add(( + registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.ReflectedImplementation, + reflectedIndex)); + } + + for (var preciseIndex = 0; + preciseIndex < registration.PreciseReflectedRegistrations.Length; + preciseIndex++) + { + orderedRegistrations.Add(( + registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.PreciseReflected, + preciseIndex)); + } + + orderedRegistrations.Sort(static (left, right) => + StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); + + var implementationVariableName = $"implementationType{registrationIndex}"; + var knownServiceTypesVariableName = $"knownServiceTypes{registrationIndex}"; + if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) + { + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = typeof("); + builder.Append(registration.ImplementationTypeDisplayName); + builder.AppendLine(");"); + } + else + { + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + } + + builder.Append(" if ("); + builder.Append(implementationVariableName); + builder.AppendLine(" is not null)"); + builder.AppendLine(" {"); + + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" var "); + builder.Append(knownServiceTypesVariableName); + builder.AppendLine(" = new global::System.Collections.Generic.HashSet();"); + foreach (var runtimeDiscoveredHandlerInterfaceLogName in registration + .RuntimeDiscoveredHandlerInterfaceLogNames) + { + builder.Append(" // Remaining runtime interface discovery target: "); + builder.Append(runtimeDiscoveredHandlerInterfaceLogName); + builder.AppendLine(); + } + } + + foreach (var orderedRegistration in orderedRegistrations) + { + switch (orderedRegistration.Kind) + { + case OrderedRegistrationKind.Direct: + var directRegistration = registration.DirectRegistrations[orderedRegistration.Index]; + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" "); + builder.Append(knownServiceTypesVariableName); + builder.Append(".Add(typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("));"); + } + + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(directRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + break; + case OrderedRegistrationKind.ReflectedImplementation: + var reflectedRegistration = + registration.ReflectedImplementationRegistrations[orderedRegistration.Index]; + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" "); + builder.Append(knownServiceTypesVariableName); + builder.Append(".Add(typeof("); + builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("));"); + } + + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.Append(" typeof("); + builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); + builder.AppendLine("),"); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + builder.Append(" logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(reflectedRegistration.HandlerInterfaceLogName)); + builder.AppendLine(".\");"); + break; + case OrderedRegistrationKind.PreciseReflected: + var preciseRegistration = registration.PreciseReflectedRegistrations[orderedRegistration.Index]; + var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistration.Index}"; + AppendPreciseReflectedTypeResolution( + builder, + preciseRegistration.ServiceTypeArguments, + registrationVariablePrefix, + implementationVariableName, + preciseRegistration.OpenHandlerTypeDisplayName, + registration.ImplementationLogName, + preciseRegistration.HandlerInterfaceLogName, + knownServiceTypesVariableName, + registration.RequiresRuntimeInterfaceDiscovery, + 3); + break; + default: + throw new InvalidOperationException( + $"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}."); + } + } + + if (registration.RequiresRuntimeInterfaceDiscovery) + { + builder.Append(" RegisterRemainingReflectedHandlerInterfaces(services, logger, "); + builder.Append(implementationVariableName); + builder.Append(", "); + builder.Append(knownServiceTypesVariableName); + builder.AppendLine(");"); + } + + builder.AppendLine(" }"); + } + + private static void AppendPreciseReflectedTypeResolution( + StringBuilder builder, + ImmutableArray serviceTypeArguments, + string registrationVariablePrefix, + string implementationVariableName, + string openHandlerTypeDisplayName, + string implementationLogName, + string handlerInterfaceLogName, + string knownServiceTypesVariableName, + bool trackKnownServiceTypes, + int indentLevel) + { + var indent = new string(' ', indentLevel * 4); + var nestedIndent = new string(' ', (indentLevel + 1) * 4); + var resolvedArgumentNames = new string[serviceTypeArguments.Length]; + var reflectedArgumentNames = new List(); + + for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++) + { + resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + serviceTypeArguments[argumentIndex], + $"{registrationVariablePrefix}Argument{argumentIndex}", + reflectedArgumentNames, + indent); + } + + if (reflectedArgumentNames.Count > 0) + { + builder.Append(indent); + builder.Append("if ("); + for (var index = 0; index < reflectedArgumentNames.Count; index++) + { + if (index > 0) + builder.Append(" && "); + + builder.Append(reflectedArgumentNames[index]); + builder.Append(" is not null"); + } + + builder.AppendLine(")"); + builder.Append(indent); + builder.AppendLine("{"); + indent = nestedIndent; + } + + builder.Append(indent); + builder.Append("var "); + builder.Append(registrationVariablePrefix); + builder.Append(" = typeof("); + builder.Append(openHandlerTypeDisplayName); + builder.Append(").MakeGenericType("); + for (var index = 0; index < resolvedArgumentNames.Length; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append(resolvedArgumentNames[index]); + } + + builder.AppendLine(");"); + builder.Append(indent); + builder.AppendLine( + "global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.Append(indent); + builder.AppendLine(" services,"); + builder.Append(indent); + builder.Append(" "); + builder.Append(registrationVariablePrefix); + builder.AppendLine(","); + builder.Append(indent); + builder.Append(" "); + builder.Append(implementationVariableName); + builder.AppendLine(");"); + if (trackKnownServiceTypes) + { + builder.Append(indent); + builder.Append(knownServiceTypesVariableName); + builder.Append(".Add("); + builder.Append(registrationVariablePrefix); + builder.AppendLine(");"); + } + + builder.Append(indent); + builder.Append("logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(implementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(handlerInterfaceLogName)); + builder.AppendLine(".\");"); + + if (reflectedArgumentNames.Count > 0) + { + builder.Append(new string(' ', indentLevel * 4)); + builder.AppendLine("}"); + } + } + + private static string AppendRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName)) + return $"typeof({runtimeTypeReference.TypeDisplayName})"; + + if (runtimeTypeReference.ArrayElementTypeReference is not null) + { + var elementExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.ArrayElementTypeReference, + $"{variableBaseName}Element", + reflectedArgumentNames, + indent); + + return runtimeTypeReference.ArrayRank == 1 + ? $"{elementExpression}.MakeArrayType()" + : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; + } + + if (runtimeTypeReference.GenericTypeDefinitionReference is not null) + { + var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeDefinitionReference, + $"{variableBaseName}GenericDefinition", + reflectedArgumentNames, + indent); + var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; + for (var argumentIndex = 0; + argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; + argumentIndex++) + { + genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeArguments[argumentIndex], + $"{variableBaseName}GenericArgument{argumentIndex}", + reflectedArgumentNames, + indent); + } + + return + $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; + } + + var reflectionTypeMetadataName = runtimeTypeReference.ReflectionTypeMetadataName!; + reflectedArgumentNames.Add(variableBaseName); + builder.Append(indent); + builder.Append("var "); + builder.Append(variableBaseName); + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(reflectionTypeMetadataName)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + return variableBaseName; + } + + private static void AppendReflectionHelpers(StringBuilder builder) + { + // Emit the runtime helper methods only when at least one handler still needs implementation-scoped + // interface discovery after all direct / precise registrations have been emitted. + builder.AppendLine( + " private static void RegisterRemainingReflectedHandlerInterfaces(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Type implementationType, global::System.Collections.Generic.ISet knownServiceTypes)"); + builder.AppendLine(" {"); + builder.AppendLine(" var handlerInterfaces = implementationType.GetInterfaces();"); + builder.AppendLine(" global::System.Array.Sort(handlerInterfaces, CompareTypes);"); + builder.AppendLine(); + builder.AppendLine(" foreach (var handlerInterface in handlerInterfaces)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (!IsSupportedHandlerInterface(handlerInterface))"); + builder.AppendLine(" continue;"); + builder.AppendLine(); + builder.AppendLine(" if (knownServiceTypes.Contains(handlerInterface))"); + builder.AppendLine(" continue;"); + builder.AppendLine(); + builder.AppendLine( + " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.AppendLine(" services,"); + builder.AppendLine(" handlerInterface,"); + builder.AppendLine(" implementationType);"); + builder.AppendLine( + " logger.Debug($\"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}.\");"); + builder.AppendLine(" knownServiceTypes.Add(handlerInterface);"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static int CompareTypes(global::System.Type left, global::System.Type right)"); + builder.AppendLine(" {"); + builder.AppendLine( + " return global::System.StringComparer.Ordinal.Compare(GetRuntimeTypeDisplayName(left), GetRuntimeTypeDisplayName(right));"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static bool IsSupportedHandlerInterface(global::System.Type interfaceType)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (!interfaceType.IsGenericType)"); + builder.AppendLine(" return false;"); + builder.AppendLine(); + builder.AppendLine(" var definitionFullName = interfaceType.GetGenericTypeDefinition().FullName;"); + builder.AppendLine( + $" return global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{IRequestHandlerMetadataName}\")"); + builder.AppendLine( + $" || global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{INotificationHandlerMetadataName}\")"); + builder.AppendLine( + $" || global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{IStreamRequestHandlerMetadataName}\");"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static string GetRuntimeTypeDisplayName(global::System.Type type)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (type == typeof(string))"); + builder.AppendLine(" return \"string\";"); + builder.AppendLine(" if (type == typeof(int))"); + builder.AppendLine(" return \"int\";"); + builder.AppendLine(" if (type == typeof(long))"); + builder.AppendLine(" return \"long\";"); + builder.AppendLine(" if (type == typeof(short))"); + builder.AppendLine(" return \"short\";"); + builder.AppendLine(" if (type == typeof(byte))"); + builder.AppendLine(" return \"byte\";"); + builder.AppendLine(" if (type == typeof(bool))"); + builder.AppendLine(" return \"bool\";"); + builder.AppendLine(" if (type == typeof(object))"); + builder.AppendLine(" return \"object\";"); + builder.AppendLine(" if (type == typeof(void))"); + builder.AppendLine(" return \"void\";"); + builder.AppendLine(" if (type == typeof(uint))"); + builder.AppendLine(" return \"uint\";"); + builder.AppendLine(" if (type == typeof(ulong))"); + builder.AppendLine(" return \"ulong\";"); + builder.AppendLine(" if (type == typeof(ushort))"); + builder.AppendLine(" return \"ushort\";"); + builder.AppendLine(" if (type == typeof(sbyte))"); + builder.AppendLine(" return \"sbyte\";"); + builder.AppendLine(" if (type == typeof(float))"); + builder.AppendLine(" return \"float\";"); + builder.AppendLine(" if (type == typeof(double))"); + builder.AppendLine(" return \"double\";"); + builder.AppendLine(" if (type == typeof(decimal))"); + builder.AppendLine(" return \"decimal\";"); + builder.AppendLine(" if (type == typeof(char))"); + builder.AppendLine(" return \"char\";"); + builder.AppendLine(); + builder.AppendLine(" if (type.IsArray)"); + builder.AppendLine(" return GetRuntimeTypeDisplayName(type.GetElementType()!) + \"[]\";"); + builder.AppendLine(); + builder.AppendLine(" if (!type.IsGenericType)"); + builder.AppendLine(" return (type.FullName ?? type.Name).Replace('+', '.');"); + builder.AppendLine(); + builder.AppendLine(" var genericTypeName = type.GetGenericTypeDefinition().FullName ?? type.Name;"); + builder.AppendLine(" var arityIndex = genericTypeName.IndexOf('`');"); + builder.AppendLine(" if (arityIndex >= 0)"); + builder.AppendLine(" genericTypeName = genericTypeName[..arityIndex];"); + builder.AppendLine(); + builder.AppendLine(" genericTypeName = genericTypeName.Replace('+', '.');"); + builder.AppendLine(" var arguments = type.GetGenericArguments();"); + builder.AppendLine(" var builder = new global::System.Text.StringBuilder();"); + builder.AppendLine(" builder.Append(genericTypeName);"); + builder.AppendLine(" builder.Append('<');"); + builder.AppendLine(); + builder.AppendLine(" for (var index = 0; index < arguments.Length; index++)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (index > 0)"); + builder.AppendLine(" builder.Append(\", \");"); + builder.AppendLine(); + builder.AppendLine(" builder.Append(GetRuntimeTypeDisplayName(arguments[index]));"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" builder.Append('>');"); + builder.AppendLine(" return builder.ToString();"); + builder.AppendLine(" }"); + } + + private static string EscapeStringLiteral(string value) + { + return value.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r"); + } + + private readonly record struct HandlerRegistrationSpec( + string HandlerInterfaceDisplayName, + string ImplementationTypeDisplayName, + string HandlerInterfaceLogName, + string ImplementationLogName); + + private readonly record struct ReflectedImplementationRegistrationSpec( + string HandlerInterfaceDisplayName, + string HandlerInterfaceLogName); + + private enum OrderedRegistrationKind + { + Direct, + ReflectedImplementation, + PreciseReflected + } + + private sealed record RuntimeTypeReferenceSpec( + string? TypeDisplayName, + string? ReflectionTypeMetadataName, + RuntimeTypeReferenceSpec? ArrayElementTypeReference, + int ArrayRank, + RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, + ImmutableArray GenericTypeArguments) + { + public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) + { + return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, 0, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) + { + return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, 0, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) + { + return new RuntimeTypeReferenceSpec(null, null, elementTypeReference, arrayRank, null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromConstructedGeneric( + RuntimeTypeReferenceSpec genericTypeDefinitionReference, + ImmutableArray genericTypeArguments) + { + return new RuntimeTypeReferenceSpec(null, null, null, 0, genericTypeDefinitionReference, + genericTypeArguments); + } + } + + private readonly record struct PreciseReflectedRegistrationSpec( + string OpenHandlerTypeDisplayName, + string HandlerInterfaceLogName, + ImmutableArray ServiceTypeArguments); + + private readonly record struct ImplementationRegistrationSpec( + string ImplementationTypeDisplayName, + string ImplementationLogName, + ImmutableArray DirectRegistrations, + ImmutableArray ReflectedImplementationRegistrations, + ImmutableArray PreciseReflectedRegistrations, + string? ReflectionTypeMetadataName, + bool RequiresRuntimeInterfaceDiscovery, + ImmutableArray RuntimeDiscoveredHandlerInterfaceLogNames); + + private readonly struct HandlerCandidateAnalysis : IEquatable + { + public HandlerCandidateAnalysis( + string implementationTypeDisplayName, + string implementationLogName, + ImmutableArray registrations, + ImmutableArray reflectedImplementationRegistrations, + ImmutableArray preciseReflectedRegistrations, + string? reflectionTypeMetadataName, + bool requiresRuntimeInterfaceDiscovery, + ImmutableArray runtimeDiscoveredHandlerInterfaceLogNames) + { + ImplementationTypeDisplayName = implementationTypeDisplayName; + ImplementationLogName = implementationLogName; + Registrations = registrations; + ReflectedImplementationRegistrations = reflectedImplementationRegistrations; + PreciseReflectedRegistrations = preciseReflectedRegistrations; + ReflectionTypeMetadataName = reflectionTypeMetadataName; + RequiresRuntimeInterfaceDiscovery = requiresRuntimeInterfaceDiscovery; + RuntimeDiscoveredHandlerInterfaceLogNames = runtimeDiscoveredHandlerInterfaceLogNames; + } + + public string ImplementationTypeDisplayName { get; } + + public string ImplementationLogName { get; } + + public ImmutableArray Registrations { get; } + + public ImmutableArray ReflectedImplementationRegistrations { get; } + + public ImmutableArray PreciseReflectedRegistrations { get; } + + public string? ReflectionTypeMetadataName { get; } + + public bool RequiresRuntimeInterfaceDiscovery { get; } + + public ImmutableArray RuntimeDiscoveredHandlerInterfaceLogNames { get; } + + public bool Equals(HandlerCandidateAnalysis other) + { + if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, + StringComparison.Ordinal) || + !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || + !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, + StringComparison.Ordinal) || + RequiresRuntimeInterfaceDiscovery != other.RequiresRuntimeInterfaceDiscovery || + Registrations.Length != other.Registrations.Length || + ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || + PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length || + RuntimeDiscoveredHandlerInterfaceLogNames.Length != + other.RuntimeDiscoveredHandlerInterfaceLogNames.Length) + { + return false; + } + + for (var index = 0; index < Registrations.Length; index++) + { + if (!Registrations[index].Equals(other.Registrations[index])) + return false; + } + + for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++) + { + if (!ReflectedImplementationRegistrations[index].Equals( + other.ReflectedImplementationRegistrations[index])) + return false; + } + + for (var index = 0; index < PreciseReflectedRegistrations.Length; index++) + { + if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index])) + return false; + } + + for (var index = 0; index < RuntimeDiscoveredHandlerInterfaceLogNames.Length; index++) + { + if (!string.Equals( + RuntimeDiscoveredHandlerInterfaceLogNames[index], + other.RuntimeDiscoveredHandlerInterfaceLogNames[index], + StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + public override bool Equals(object? obj) + { + return obj is HandlerCandidateAnalysis other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName); + hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName); + hashCode = (hashCode * 397) ^ + (ReflectionTypeMetadataName is null + ? 0 + : StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName)); + hashCode = (hashCode * 397) ^ RequiresRuntimeInterfaceDiscovery.GetHashCode(); + foreach (var registration in Registrations) + { + hashCode = (hashCode * 397) ^ registration.GetHashCode(); + } + + foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations) + { + hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode(); + } + + foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations) + { + hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode(); + } + + foreach (var runtimeDiscoveredHandlerInterfaceLogName in RuntimeDiscoveredHandlerInterfaceLogNames) + { + hashCode = (hashCode * 397) ^ + StringComparer.Ordinal.GetHashCode(runtimeDiscoveredHandlerInterfaceLogName); + } + + return hashCode; + } + } + } + + private readonly record struct GenerationEnvironment(bool GenerationEnabled); +} diff --git a/GFramework.Tests.Common/CqrsTestRuntime.cs b/GFramework.Tests.Common/CqrsTestRuntime.cs new file mode 100644 index 00000000..6109bd54 --- /dev/null +++ b/GFramework.Tests.Common/CqrsTestRuntime.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Cqrs; +using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Command; +using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime; + +namespace GFramework.Tests.Common; + +/// +/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。 +/// +/// +/// 该测试基础设施位于独立模块中,避免多个测试项目复制同一份反射绑定与默认 runtime 接线逻辑。 +/// 测试应通过该入口驱动注册流程,而不是各自维护一份实现细节副本。 +/// +public static class CqrsTestRuntime +{ + private static readonly Assembly CqrsRuntimeAssembly = typeof(CommandBase<,>).Assembly; + + private static readonly Type CqrsHandlerRegistrarType = CqrsRuntimeAssembly + .GetType( + "GFramework.Cqrs.Internal.CqrsHandlerRegistrar", + throwOnError: true)!; + + private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType + .GetMethod( + "RegisterHandlers", + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static, + binder: null, + [ + typeof(IIocContainer), + typeof(IEnumerable), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + + /// + /// 为裸测试容器补齐默认 CQRS runtime seam。 + /// + /// 目标测试容器。 + /// + /// + /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, + /// 而无需完整启动服务模块管理器。 + /// 该方法按服务类型执行幂等注册,只会补齐当前容器中尚未接线的 CQRS 基础设施。 + /// + public static void RegisterInfrastructure(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + if (container.Get() is null) + { + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger); + container.Register(runtime); + container.Register((LegacyICqrsRuntime)runtime); + } + else if (container.Get() is null) + { + container.Register((LegacyICqrsRuntime)container.GetRequired()); + } + + if (container.Get() is null) + { + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); + var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); + container.Register(registrar); + } + + if (container.Get() is null) + { + var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService"); + var registrar = container.GetRequired(); + var registrationService = CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger); + container.Register(registrationService); + } + } + + /// + /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 + /// + /// 承载处理器映射的测试容器。 + /// 要扫描的程序集集合。 + /// + /// 。 + /// + /// 反射调用底层 CQRS 处理器注册入口失败时抛出。 + /// + /// 该入口会自动调用 ,因此测试通常无需预先手动接线 CQRS 基础设施。 + /// 程序集去重与空元素过滤由生产注册入口统一处理,避免测试辅助层复制相同筛选逻辑。 + /// + public static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies) + { + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(assemblies); + + RegisterInfrastructure(container); + + var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + RegisterHandlersMethod.Invoke( + null, + [container, assemblies, logger]); + } +} diff --git a/GFramework.Tests.Common/GFramework.Tests.Common.csproj b/GFramework.Tests.Common/GFramework.Tests.Common.csproj new file mode 100644 index 00000000..ddd8c02c --- /dev/null +++ b/GFramework.Tests.Common/GFramework.Tests.Common.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + disable + enable + true + false + + + + + + + + + + diff --git a/GFramework.csproj b/GFramework.csproj index 76f9a088..b9c6c7a4 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -63,6 +63,10 @@ + + + + @@ -104,6 +108,10 @@ + + + + @@ -131,6 +139,10 @@ + + + + diff --git a/GFramework.sln b/GFramework.sln index 6d4eb45f..bc64ce21 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -38,6 +38,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGene EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstractions", "GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj", "{69C06523-98AA-49DE-95D4-4BF203716DD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramework.Cqrs\GFramework.Cqrs.csproj", "{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Tests", "GFramework.Cqrs.Tests\GFramework.Cqrs.Tests.csproj", "{29037A55-9A89-425C-AB33-D44872B2E601}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Tests.Common", "GFramework.Tests.Common\GFramework.Tests.Common.csproj", "{1100EE3E-A12D-4DE5-ABA8-591D3126570B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -276,6 +284,54 @@ Global {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x64.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.ActiveCfg = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Debug|x86.Build.0 = Debug|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|Any CPU.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x64.Build.0 = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.ActiveCfg = Release|Any CPU + {29037A55-9A89-425C-AB33-D44872B2E601}.Release|x86.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x64.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Debug|x86.Build.0 = Debug|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|Any CPU.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x64.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x64.Build.0 = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x86.ActiveCfg = Release|Any CPU + {1100EE3E-A12D-4DE5-ABA8-591D3126570B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index d0cf0f9e..9d2410fe 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -1,21 +1,21 @@ --- -title: CQRS 与 Mediator -description: CQRS 模式通过 Mediator 实现命令查询职责分离,提供清晰的业务逻辑组织方式。 +title: CQRS +description: GFramework 内建 CQRS runtime,用统一请求分发、通知发布和流式处理组织业务逻辑。 --- -# CQRS 与 Mediator +# CQRS ## 概述 CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,将数据的读取(Query)和修改(Command)操作分离。GFramework -通过集成 Mediator 库实现了 CQRS 模式,提供了类型安全、解耦的业务逻辑处理方式。 +当前内建自有 CQRS runtime,通过统一的请求分发器、通知发布和流式请求管道提供类型安全、解耦的业务逻辑处理方式。 通过 CQRS,你可以将复杂的业务逻辑拆分为独立的命令和查询处理器,每个处理器只负责单一职责,使代码更易于测试和维护。 **主要特性**: - 命令查询职责分离 -- 基于 Mediator 模式的解耦设计 +- 内建请求分发与解耦设计 - 支持管道行为(Behaviors) - 异步处理支持 - 与架构系统深度集成 @@ -72,7 +72,6 @@ public class GetPlayerQuery : QueryBase ```csharp using GFramework.Core.CQRS.Command; -using Mediator; // 命令处理器 public class CreatePlayerCommandHandler : AbstractCommandHandler @@ -92,19 +91,19 @@ public class CreatePlayerCommandHandler : AbstractCommandHandler // 4. 发送命令 public async Task SaveGame() { - var mediator = this.GetService(); - var command = new SaveGameCommand(new SaveGameInput { SlotId = 1, Data = currentGameData }); - await mediator.Send(command); + await this.SendAsync(command); } ``` @@ -195,37 +192,63 @@ public class GetHighScoresQueryHandler : AbstractQueryHandler> GetHighScores() { - var mediator = this.GetService(); - var query = new GetHighScoresQuery(new GetHighScoresInput { Count = 10 }); - var scores = await mediator.Send(query); + var scores = await this.SendQueryAsync(query); return scores; } ``` ### 注册处理器 -在架构中注册 Mediator 和处理器: +在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: ```csharp public class GameArchitecture : Architecture { - protected override void Init() + protected override void OnInitialize() { // 注册通用开放泛型行为 - RegisterMediatorBehavior>(); - RegisterMediatorBehavior>(); + RegisterCqrsPipelineBehavior>(); + RegisterCqrsPipelineBehavior>(); - // 处理器会自动通过依赖注入注册 + // 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器 } } ``` -`RegisterMediatorBehavior()` 同时支持两种形式: +当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器; +如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。 +`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。 + +如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围: + +```csharp +public class GameArchitecture : Architecture +{ + protected override void OnInitialize() + { + RegisterCqrsPipelineBehavior>(); + + RegisterCqrsHandlersFromAssemblies( + [ + typeof(InventoryCqrsMarker).Assembly, + typeof(BattleCqrsMarker).Assembly + ]); + } +} +``` + +`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑: +优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册 +handler。 + +`RegisterCqrsPipelineBehavior()` 是推荐入口;旧的 `RegisterMediatorBehavior()` +仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。 +`ContextAwareMediator*Extensions` 与 `MediatorCoroutineExtensions` 也遵循同样的弃用节奏。当前接口支持两种形式: - 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 - 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` @@ -326,7 +349,7 @@ var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput NewLevel = 10 }); -await mediator.Publish(notification); +await this.PublishAsync(notification); ``` ### Pipeline Behaviors(管道行为) @@ -334,16 +357,16 @@ await mediator.Publish(notification); Behaviors 可以在处理器执行前后添加横切关注点: ```csharp -using Mediator; +using GFramework.Core.Abstractions.Cqrs; // 日志行为 public class LoggingBehavior : IPipelineBehavior - where TMessage : IMessage + where TMessage : IRequest { public async ValueTask Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { var messageName = message.GetType().Name; Console.WriteLine($"[开始] {messageName}"); @@ -358,12 +381,12 @@ public class LoggingBehavior : IPipelineBehavior : IPipelineBehavior - where TMessage : IMessage + where TMessage : IRequest { public async ValueTask Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); @@ -382,20 +405,20 @@ public class PerformanceBehavior : IPipelineBehavior>(); -RegisterMediatorBehavior>(); +RegisterCqrsPipelineBehavior>(); +RegisterCqrsPipelineBehavior>(); ``` ### 验证行为 ```csharp public class ValidationBehavior : IPipelineBehavior - where TMessage : IMessage + where TMessage : IRequest { public async ValueTask Handle( TMessage message, - CancellationToken cancellationToken, - MessageHandlerDelegate next) + MessageHandlerDelegate next, + CancellationToken cancellationToken) { // 验证输入 if (message is IValidatable validatable) @@ -441,7 +464,7 @@ public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler>(); - RegisterMediatorBehavior>(); + RegisterCqrsPipelineBehavior>(); + RegisterCqrsPipelineBehavior>(); ``` 5. **保持处理器简单**:一个处理器只做一件事 @@ -530,12 +553,12 @@ CalculateDamageRequest **解答**: -- **Notification**:通过 Mediator 发送,处理器在同一请求上下文中执行 +- **Notification**:通过内建 CQRS runtime 发送,处理器在同一请求上下文中执行 - **Event**:通过 EventBus 发送,监听器异步执行 ```csharp // Notification: 同步处理 -await mediator.Publish(notification); // 等待所有处理器完成 +await this.PublishAsync(notification); // 等待所有处理器完成 // Event: 异步处理 this.SendEvent(event); // 立即返回,监听器异步执行 @@ -569,15 +592,13 @@ public override async ValueTask Handle(...) ### 问题:处理器可以调用其他处理器吗? **解答**: -可以,通过 Mediator 发送新的命令或查询: +可以,通过架构上下文继续发送新的命令或查询: ```csharp public override async ValueTask Handle(...) { - var mediator = this.GetService(); - // 调用其他命令 - await mediator.Send(new AnotherCommand(...)); + await this.SendAsync(new AnotherCommand(...)); return Unit.Value; } diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md index 8430e58f..5c4837bf 100644 --- a/docs/zh-CN/core/index.md +++ b/docs/zh-CN/core/index.md @@ -391,17 +391,17 @@ public class PlayerController : IController #### 5. ArchitectureModules (模块管理器) -**职责**: 管理架构模块和中介行为 +**职责**: 管理架构模块和 CQRS 管道行为 **核心功能**: - 模块安装 (IArchitectureModule) -- 中介行为注册 (Mediator Behaviors) +- CQRS 管道行为注册(推荐 API 为 `RegisterCqrsPipelineBehavior`) **关键方法**: - `InstallModule()` - 安装模块 -- `RegisterMediatorBehavior()` - 注册中介行为 +- `RegisterCqrsPipelineBehavior()` - 注册 CQRS 管道行为 #### 设计优势 @@ -672,4 +672,3 @@ public interface IController : - 添加 `PhaseChanged` 事件,支持阶段监听 **向后兼容**: 所有公共 API 保持不变,现有代码无需修改。 - diff --git a/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md b/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md index 2d73f531..8d1ca366 100644 --- a/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md +++ b/docs/zh-CN/source-generators/auto-register-exported-collections-generator.md @@ -13,12 +13,13 @@ `AutoRegisterExportedCollections` 会把这类样板收敛成声明式配置。 它特别适合 `GameEntryPoint`、资源根节点、配置引导节点这类“导出即注册”的场景。 +相关特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp using System.Collections.Generic; -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; public interface IKeyValue diff --git a/docs/zh-CN/source-generators/auto-scene-generator.md b/docs/zh-CN/source-generators/auto-scene-generator.md index 1a2ff881..92927daa 100644 --- a/docs/zh-CN/source-generators/auto-scene-generator.md +++ b/docs/zh-CN/source-generators/auto-scene-generator.md @@ -12,11 +12,12 @@ - `GetScene()` 包装方法 `AutoScene` 会在编译期生成这些固定样板。 +该特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; diff --git a/docs/zh-CN/source-generators/auto-ui-page-generator.md b/docs/zh-CN/source-generators/auto-ui-page-generator.md index b455eab8..1c1ecd48 100644 --- a/docs/zh-CN/source-generators/auto-ui-page-generator.md +++ b/docs/zh-CN/source-generators/auto-ui-page-generator.md @@ -13,11 +13,12 @@ - `GetPage()` 工厂包装 `AutoUiPage` 会把这部分样板迁移到编译期生成。 +该特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。 ## 基础使用 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -123,7 +124,7 @@ partial class MainMenu ## 组合示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using GFramework.SourceGenerators.Abstractions.Rule; using Godot; diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md index 8fd91e66..ea62aad0 100644 --- a/docs/zh-CN/source-generators/index.md +++ b/docs/zh-CN/source-generators/index.md @@ -610,7 +610,7 @@ AutoUiPage 生成器为 Godot 页面节点自动生成 `UiKeyStr`、缓存的 `I ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -635,7 +635,7 @@ AutoScene 生成器为场景根节点自动生成 `SceneKeyStr`、缓存的 `ISc ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using GFramework.Game.Abstractions.Enums; using Godot; @@ -660,7 +660,7 @@ AutoRegisterExportedCollections 生成器为 Godot 导出集合自动生成批 ### 基础示例 ```csharp -using GFramework.Godot.SourceGenerators.Abstractions; +using GFramework.Godot.SourceGenerators.Abstractions.UI; using Godot; [AutoRegisterExportedCollections]