mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-08 01:24:31 +08:00
Merge pull request #223 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-4
feat(core): 扩展 CQRS 处理器注册 API 并完善文档
This commit is contained in:
commit
3dbe1053fb
20
.github/workflows/auto-tag.yml
vendored
20
.github/workflows/auto-tag.yml
vendored
@ -5,6 +5,8 @@ on:
|
||||
workflows: ["CI - Build & Test"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: auto-tag-main
|
||||
@ -13,15 +15,15 @@ concurrency:
|
||||
jobs:
|
||||
auto-tag:
|
||||
if: >
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(
|
||||
(
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
contains(github.event.workflow_run.head_commit.message, '[release ci]')
|
||||
)
|
||||
||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
contains(github.event.workflow_run.head_commit.message, '[release ci]')
|
||||
)
|
||||
||
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.ref == 'refs/heads/main'
|
||||
)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
@ -61,4 +63,4 @@ jobs:
|
||||
fi
|
||||
|
||||
git tag -a "$TAG" -m "Auto tag $TAG"
|
||||
git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG"
|
||||
git push "https://x-access-token:${PAT}@github.com/${{ github.repository }}.git" "$TAG"
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -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:
|
||||
|
||||
9
.github/workflows/codeql.yml
vendored
9
.github/workflows/codeql.yml
vendored
@ -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 * * *'
|
||||
|
||||
|
||||
@ -105,7 +105,8 @@ major 版本中移除。
|
||||
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
|
||||
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
|
||||
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
|
||||
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。
|
||||
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描;非默认程序集可通过
|
||||
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 显式接入同一路径。
|
||||
|
||||
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。
|
||||
|
||||
|
||||
@ -1,9 +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;
|
||||
|
||||
@ -96,6 +96,24 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
|
||||
void RegisterMediatorBehavior<TBehavior>()
|
||||
where TBehavior : class;
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 当处理器位于默认架构程序集之外的模块或扩展程序集中时,可在初始化阶段调用该入口接入对应程序集。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
|
||||
void RegisterCqrsHandlersFromAssembly(Assembly assembly);
|
||||
|
||||
/// <summary>
|
||||
/// 从多个程序集显式注册 CQRS 处理器。
|
||||
/// 该入口会对程序集集合去重,适用于统一接入多个扩展包或模块程序集。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
|
||||
void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies);
|
||||
|
||||
/// <summary>
|
||||
/// 安装架构模块
|
||||
/// </summary>
|
||||
|
||||
@ -16,4 +16,5 @@ global using System.Collections.Generic;
|
||||
global using System.Runtime;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using System.Threading.Tasks;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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;
|
||||
|
||||
@ -109,6 +109,25 @@ public interface IIocContainer : IContextAware
|
||||
void RegisterMediatorBehavior<TBehavior>()
|
||||
where TBehavior : class;
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 该入口适用于处理器不位于默认架构程序集中的场景,例如扩展包、模块程序集或拆分后的业务程序集。
|
||||
/// 运行时会优先使用程序集级源码生成注册器;若不存在可用注册器,则自动回退到反射扫描。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
|
||||
void RegisterCqrsHandlersFromAssembly(Assembly assembly);
|
||||
|
||||
/// <summary>
|
||||
/// 从多个程序集显式注册 CQRS 处理器。
|
||||
/// 容器会按稳定程序集键去重,避免默认启动路径与扩展模块重复接入同一程序集时产生重复 handler 映射。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
|
||||
void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 配置服务
|
||||
|
||||
@ -0,0 +1,200 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Logging;
|
||||
|
||||
namespace GFramework.Core.Tests.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 验证架构初始化阶段可以显式接入默认程序集之外的 CQRS handlers。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class ArchitectureAdditionalCqrsHandlersTests
|
||||
{
|
||||
private ILoggerFactoryProvider? _previousLoggerFactoryProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化日志工厂和共享测试状态。
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_previousLoggerFactoryProvider = LoggerFactoryResolver.Provider;
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
GameContext.Clear();
|
||||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理测试过程中写入的共享状态。
|
||||
/// </summary>
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
AdditionalAssemblyNotificationHandlerState.Reset();
|
||||
GameContext.Clear();
|
||||
LoggerFactoryResolver.Provider = _previousLoggerFactoryProvider
|
||||
?? throw new InvalidOperationException(
|
||||
"LoggerFactoryResolver.Provider should be captured during setup.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。
|
||||
/// </summary>
|
||||
/// <returns>The asynchronous test task.</returns>
|
||||
[Test]
|
||||
public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedHandlerAssembly();
|
||||
var architecture = CreateArchitecture(target =>
|
||||
target.RegisterCqrsHandlersFromAssembly(generatedAssembly.Object));
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
|
||||
|
||||
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证不同 <see cref="Assembly" /> 实例只要解析到相同程序集键,就不会向容器重复写入相同 handler 映射。
|
||||
/// </summary>
|
||||
/// <returns>The asynchronous test task.</returns>
|
||||
[Test]
|
||||
public async Task RegisterCqrsHandlersFromAssembly_Should_Deduplicate_Repeated_Assembly_Registration()
|
||||
{
|
||||
var generatedAssemblyA = CreateGeneratedHandlerAssembly();
|
||||
var generatedAssemblyB = CreateGeneratedHandlerAssembly();
|
||||
var architecture = CreateArchitecture(target =>
|
||||
{
|
||||
target.RegisterCqrsHandlersFromAssembly(generatedAssemblyA.Object);
|
||||
target.RegisterCqrsHandlersFromAssemblies([generatedAssemblyB.Object]);
|
||||
});
|
||||
|
||||
await architecture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
await architecture.Context.PublishAsync(new AdditionalAssemblyNotification());
|
||||
|
||||
Assert.That(AdditionalAssemblyNotificationHandlerState.InvocationCount, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await architecture.DestroyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个仅暴露程序集级 CQRS registry 元数据的 mocked Assembly。
|
||||
/// 该测试替身模拟“扩展程序集已经挂接 source-generator,运行时只需显式接入该程序集”的真实路径。
|
||||
/// </summary>
|
||||
/// <returns>包含程序集级 handler registry 元数据的 mocked Assembly。</returns>
|
||||
private static Mock<Assembly> CreateGeneratedHandlerAssembly()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns("GFramework.Core.Tests.Architectures.ExplicitAdditionalHandlers, Version=1.0.0.0");
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(AdditionalAssemblyNotificationHandlerRegistry))]);
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建复用现有测试架构基建的测试架构,并在注册阶段后执行额外程序集接入逻辑。
|
||||
/// </summary>
|
||||
/// <param name="configure">初始化阶段执行的额外 CQRS 程序集接入逻辑。</param>
|
||||
/// <returns>带有注册后钩子的测试架构实例。</returns>
|
||||
private static SyncTestArchitecture CreateArchitecture(Action<TestArchitectureBase> configure)
|
||||
{
|
||||
var architecture = new SyncTestArchitecture();
|
||||
architecture.AddPostRegistrationHook(configure);
|
||||
return architecture;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证额外程序集接入是否成功的测试通知。
|
||||
/// </summary>
|
||||
public sealed record AdditionalAssemblyNotification : INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 记录模拟扩展程序集通知处理器的执行次数。
|
||||
/// </summary>
|
||||
public static class AdditionalAssemblyNotificationHandlerState
|
||||
{
|
||||
private static int _invocationCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前测试进程中该处理器的执行次数。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该计数器通过原子读写维护,以支持 NUnit 并行执行环境中的并发访问。
|
||||
/// </remarks>
|
||||
public static int InvocationCount => Volatile.Read(ref _invocationCount);
|
||||
|
||||
/// <summary>
|
||||
/// 记录一次通知处理,供测试断言显式程序集接入后的运行时行为。
|
||||
/// </summary>
|
||||
public static void RecordInvocation()
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理共享计数器,避免测试间相互污染。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
Interlocked.Exchange(ref _invocationCount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟由 source-generator 为扩展程序集生成的 CQRS handler registry。
|
||||
/// </summary>
|
||||
internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandlerRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 将扩展程序集中的通知处理器映射写入服务集合。
|
||||
/// </summary>
|
||||
/// <param name="services">目标服务集合。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// 当 <paramref name="services" /> 或 <paramref name="logger" /> 为 <see langword="null" /> 时抛出。
|
||||
/// </exception>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient<INotificationHandler<AdditionalAssemblyNotification>>(_ => CreateHandler());
|
||||
logger.Debug(
|
||||
$"Registered CQRS handler proxy for {typeof(INotificationHandler<AdditionalAssemblyNotification>).FullName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个仅供显式程序集注册路径使用的动态通知处理器。
|
||||
/// </summary>
|
||||
/// <returns>用于记录通知触发次数的测试替身处理器。</returns>
|
||||
private static INotificationHandler<AdditionalAssemblyNotification> CreateHandler()
|
||||
{
|
||||
var handler = new Mock<INotificationHandler<AdditionalAssemblyNotification>>();
|
||||
handler
|
||||
.Setup(target => target.Handle(It.IsAny<AdditionalAssemblyNotification>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(() =>
|
||||
{
|
||||
AdditionalAssemblyNotificationHandlerState.RecordInvocation();
|
||||
return ValueTask.CompletedTask;
|
||||
});
|
||||
return handler.Object;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Lifecycle;
|
||||
@ -185,6 +186,26 @@ public class TestArchitectureWithRegistry : IArchitecture
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
|
||||
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
@ -316,6 +337,26 @@ public class TestArchitectureWithoutRegistry : IArchitecture
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
|
||||
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Bases;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Tests.Cqrs;
|
||||
using GFramework.Core.Tests.Systems;
|
||||
|
||||
namespace GFramework.Core.Tests.Ioc;
|
||||
@ -306,6 +308,34 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(_container.Contains<TestService>(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试清空容器后可以重新接入同一程序集中的 CQRS 处理器。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State()
|
||||
{
|
||||
var assembly = typeof(CqrsHandlerRegistrarTests).Assembly;
|
||||
|
||||
_container.RegisterCqrsHandlersFromAssembly(assembly);
|
||||
Assert.That(
|
||||
_container.GetServicesUnsafe.Any(static descriptor =>
|
||||
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
|
||||
Is.True);
|
||||
|
||||
_container.Clear();
|
||||
Assert.That(
|
||||
_container.GetServicesUnsafe.Any(static descriptor =>
|
||||
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
|
||||
Is.False);
|
||||
|
||||
_container.RegisterCqrsHandlersFromAssembly(assembly);
|
||||
|
||||
Assert.That(
|
||||
_container.GetServicesUnsafe.Any(static descriptor =>
|
||||
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
|
||||
Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试冻结容器以防止进一步注册的功能
|
||||
/// </summary>
|
||||
@ -676,4 +706,4 @@ public sealed class PrioritizedService : IPrioritizedService, IMixedService
|
||||
public sealed class NonPrioritizedService : IMixedService
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
@ -169,6 +170,30 @@ public abstract class Architecture : IArchitecture
|
||||
RegisterCqrsPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 该入口适用于把拆分到其他模块或扩展包程序集中的 handlers 接入当前架构。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
|
||||
{
|
||||
_modules.RegisterCqrsHandlersFromAssembly(assembly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从多个程序集显式注册 CQRS 处理器。
|
||||
/// 适用于在初始化阶段批量接入多个扩展程序集,并沿用容器的去重策略避免重复注册。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">当前架构的底层容器已冻结,无法继续注册处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
_modules.RegisterCqrsHandlersFromAssemblies(assemblies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装架构模块
|
||||
/// </summary>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Cqrs.Internal;
|
||||
|
||||
namespace GFramework.Core.Architectures;
|
||||
|
||||
@ -99,10 +98,11 @@ internal sealed class ArchitectureBootstrapper(
|
||||
private void ConfigureServices(IArchitectureContext context, Action<IServiceCollection>? configurator)
|
||||
{
|
||||
services.SetContext(context);
|
||||
CqrsHandlerRegistrar.RegisterHandlers(
|
||||
services.Container,
|
||||
[architectureType.Assembly, typeof(ArchitectureContext).Assembly],
|
||||
logger);
|
||||
services.Container.RegisterCqrsHandlersFromAssemblies(
|
||||
[
|
||||
architectureType.Assembly,
|
||||
typeof(ArchitectureContext).Assembly
|
||||
]);
|
||||
|
||||
if (configurator is null)
|
||||
logger.Debug("No external service configurator provided. Using built-in CQRS runtime registration only.");
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
@ -38,6 +39,34 @@ internal sealed class ArchitectureModules(
|
||||
RegisterCqrsPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// 该入口用于把默认架构程序集之外的扩展处理器接入当前架构容器。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
logger.Debug($"Registering CQRS handlers from assembly: {assembly.FullName ?? assembly.GetName().Name}");
|
||||
services.Container.RegisterCqrsHandlersFromAssembly(assembly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从多个程序集显式注册 CQRS 处理器。
|
||||
/// 它会复用容器级去重逻辑,避免模块重复接入相同程序集时重复注册 handler。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">底层容器已冻结,无法继续注册处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
logger.Debug("Registering CQRS handlers from additional assemblies.");
|
||||
services.Container.RegisterCqrsHandlersFromAssemblies(assemblies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装架构模块
|
||||
/// </summary>
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Bases;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Cqrs.Internal;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Rule;
|
||||
|
||||
@ -56,6 +58,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
private readonly HashSet<object> _registeredInstances = [];
|
||||
|
||||
/// <summary>
|
||||
/// 已接入 CQRS handler 注册流程的程序集键集合。
|
||||
/// 使用稳定字符串键而不是 Assembly 引用本身,以避免默认路径和显式扩展路径使用不同 Assembly 对象时重复注册。
|
||||
/// </summary>
|
||||
private readonly HashSet<string> _registeredCqrsHandlerAssemblyKeys = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// 日志记录器,用于记录容器操作日志
|
||||
/// </summary>
|
||||
@ -372,6 +380,60 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
RegisterCqrsPipelineBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定程序集显式注册 CQRS 处理器。
|
||||
/// </summary>
|
||||
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assembly" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
RegisterCqrsHandlersFromAssemblies([assembly]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从多个程序集显式注册 CQRS 处理器。
|
||||
/// 同一程序集只会被接入一次,避免默认启动路径与扩展模块重复注册相同 handlers。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
|
||||
var processedAssemblyKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var assembly in assemblies
|
||||
.Where(static assembly => assembly is not null)
|
||||
.OrderBy(GetCqrsAssemblyRegistrationKey, StringComparer.Ordinal))
|
||||
{
|
||||
var assemblyKey = GetCqrsAssemblyRegistrationKey(assembly);
|
||||
if (!processedAssemblyKeys.Add(assemblyKey))
|
||||
continue;
|
||||
|
||||
if (_registeredCqrsHandlerAssemblyKeys.Contains(assemblyKey))
|
||||
{
|
||||
_logger.Debug(
|
||||
$"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered.");
|
||||
continue;
|
||||
}
|
||||
|
||||
CqrsHandlerRegistrar.RegisterHandlers(this, [assembly], _logger);
|
||||
_registeredCqrsHandlerAssemblyKeys.Add(assemblyKey);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置服务
|
||||
/// </summary>
|
||||
@ -745,6 +807,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_registeredCqrsHandlerAssemblyKeys.Clear();
|
||||
_provider = null;
|
||||
_logger.Info("Container cleared");
|
||||
}
|
||||
@ -816,5 +879,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CQRS handler 注册用的稳定程序集键。
|
||||
/// 该键需要同时兼顾真实程序集与测试中使用的 mocked Assembly,避免仅靠引用比较导致重复接入。
|
||||
/// </summary>
|
||||
/// <param name="assembly">目标程序集。</param>
|
||||
/// <returns>稳定的程序集标识字符串。</returns>
|
||||
private static string GetCqrsAssemblyRegistrationKey(Assembly assembly)
|
||||
{
|
||||
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -224,7 +224,27 @@ public class GameArchitecture : Architecture
|
||||
如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。
|
||||
`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。
|
||||
|
||||
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围。
|
||||
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围:
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
||||
|
||||
RegisterCqrsHandlersFromAssemblies(
|
||||
[
|
||||
typeof(InventoryCqrsMarker).Assembly,
|
||||
typeof(BattleCqrsMarker).Assembly
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑:
|
||||
优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册
|
||||
handler。
|
||||
|
||||
`RegisterCqrsPipelineBehavior<TBehavior>()` 是推荐入口;旧的 `RegisterMediatorBehavior<TBehavior>()`
|
||||
仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user