mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(ioc): 添加 Microsoft DI 容器适配器和 CI/CD 工作流
- 实现 MicrosoftDiContainer 类,提供 Microsoft.Extensions.DependencyInjection 的适配器 - 添加 DefaultCqrsHandlerRegistrar 默认 CQRS 处理器注册器实现 - 配置 GitHub Actions CI/CD 工作流,包含代码质量检查和构建测试任务 - 设置 .NET 8/9/10 多版本支持和缓存策略 - 添加单元测试覆盖 IoC 容器的各项功能,包括注册、解析和生命周期管理 - 实现线程安全的读写锁机制保护容器操作 - 支持 CQRS 处理器和管道行为的注册管理
This commit is contained in:
parent
048f96c6cd
commit
34e140e919
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -166,6 +166,12 @@ jobs:
|
||||
--logger "trx;LogFileName=sg-$RANDOM.trx" \
|
||||
--results-directory TestResults &
|
||||
|
||||
dotnet test GFramework.Cqrs.Tests \
|
||||
-c Release \
|
||||
--no-build \
|
||||
--logger "trx;LogFileName=cqrs-$RANDOM.trx" \
|
||||
--results-directory TestResults &
|
||||
|
||||
dotnet test GFramework.Ecs.Arch.Tests \
|
||||
-c Release \
|
||||
--no-build \
|
||||
|
||||
36
GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs
Normal file
36
GFramework.Core.Tests/Cqrs/ContainerRegistrationFixtures.cs
Normal file
@ -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.
|
||||
|
||||
namespace GFramework.Core.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为容器层测试提供可扫描的最小通知夹具。
|
||||
/// </summary>
|
||||
internal sealed record DeterministicOrderNotification : INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 供容器注册测试验证程序集扫描结果的通知处理器。
|
||||
/// </summary>
|
||||
internal sealed class DeterministicOrderNotificationHandler : INotificationHandler<DeterministicOrderNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// 无副作用地消费通知。
|
||||
/// </summary>
|
||||
/// <param name="notification">通知实例。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>已完成任务。</returns>
|
||||
public ValueTask Handle(DeterministicOrderNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -315,7 +315,7 @@ public class MicrosoftDiContainerTests
|
||||
[Test]
|
||||
public void Clear_Should_Reset_Cqrs_Assembly_Deduplication_State()
|
||||
{
|
||||
var assembly = typeof(CqrsHandlerRegistrarTests).Assembly;
|
||||
var assembly = typeof(DeterministicOrderNotification).Assembly;
|
||||
|
||||
_container.RegisterCqrsHandlersFromAssembly(assembly);
|
||||
Assert.That(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Architectures;
|
||||
117
GFramework.Cqrs.Tests/CqrsTestRuntime.cs
Normal file
117
GFramework.Cqrs.Tests/CqrsTestRuntime.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
|
||||
namespace GFramework.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 为测试项目提供对 CQRS 处理器真实注册入口的受控访问。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 测试应通过该入口驱动注册流程,而不是直接反射调用注册器的私有辅助方法,
|
||||
/// 这样可以覆盖生产启动路径中的程序集去重、日志记录与容错恢复行为。
|
||||
/// </remarks>
|
||||
internal static class CqrsTestRuntime
|
||||
{
|
||||
private static readonly Type CqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
|
||||
.GetType(
|
||||
"GFramework.Core.Cqrs.Internal.CqrsHandlerRegistrar",
|
||||
throwOnError: true)!
|
||||
?? throw new InvalidOperationException(
|
||||
"Failed to locate CqrsHandlerRegistrar type.");
|
||||
|
||||
private static readonly MethodInfo RegisterHandlersMethod = CqrsHandlerRegistrarType
|
||||
.GetMethod(
|
||||
"RegisterHandlers",
|
||||
BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Static,
|
||||
binder: null,
|
||||
[
|
||||
typeof(IIocContainer),
|
||||
typeof(IEnumerable<Assembly>),
|
||||
typeof(ILogger)
|
||||
],
|
||||
modifiers: null)
|
||||
?? throw new InvalidOperationException(
|
||||
"Failed to locate CqrsHandlerRegistrar.RegisterHandlers.");
|
||||
|
||||
private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly
|
||||
.GetType(
|
||||
"GFramework.Core.Cqrs.Internal.CqrsDispatcher",
|
||||
throwOnError: true)!
|
||||
?? throw new InvalidOperationException(
|
||||
"Failed to locate CqrsDispatcher type.");
|
||||
|
||||
private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor(
|
||||
BindingFlags.Instance |
|
||||
BindingFlags.Public |
|
||||
BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
[
|
||||
typeof(IIocContainer),
|
||||
typeof(ILogger)
|
||||
],
|
||||
modifiers: null)
|
||||
?? throw new InvalidOperationException(
|
||||
"Failed to locate CqrsDispatcher constructor.");
|
||||
|
||||
private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly
|
||||
.GetType(
|
||||
"GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar",
|
||||
throwOnError: true)!
|
||||
?? throw new InvalidOperationException(
|
||||
"Failed to locate DefaultCqrsHandlerRegistrar type.");
|
||||
|
||||
private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor =
|
||||
DefaultCqrsHandlerRegistrarType.GetConstructor(
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
[
|
||||
typeof(IIocContainer),
|
||||
typeof(ILogger)
|
||||
],
|
||||
modifiers: null)
|
||||
?? throw new InvalidOperationException(
|
||||
"Failed to locate DefaultCqrsHandlerRegistrar constructor.");
|
||||
|
||||
/// <summary>
|
||||
/// 为裸测试容器补齐默认 CQRS runtime seam。
|
||||
/// 这使仅使用 <see cref="MicrosoftDiContainer" /> 的测试环境也能观察与生产路径一致的 runtime 行为,
|
||||
/// 而无需完整启动服务模块管理器。
|
||||
/// </summary>
|
||||
/// <param name="container">目标测试容器。</param>
|
||||
internal static void RegisterInfrastructure(MicrosoftDiContainer container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
|
||||
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime));
|
||||
var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]);
|
||||
var registrar =
|
||||
(ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]);
|
||||
|
||||
container.Register<ICqrsRuntime>(runtime);
|
||||
container.Register<ICqrsHandlerRegistrar>(registrar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。
|
||||
/// </summary>
|
||||
/// <param name="container">承载处理器映射的测试容器。</param>
|
||||
/// <param name="assemblies">要扫描的程序集集合。</param>
|
||||
internal static void RegisterHandlers(MicrosoftDiContainer container, params Assembly[] assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
|
||||
RegisterInfrastructure(container);
|
||||
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime));
|
||||
RegisterHandlersMethod.Invoke(
|
||||
null,
|
||||
[container, assemblies.Where(static assembly => assembly is not null).Distinct().ToArray(), logger]);
|
||||
}
|
||||
}
|
||||
26
GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj
Normal file
26
GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj
Normal file
@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<WarningLevel>0</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>
|
||||
<PackageReference Include="Moq" Version="4.20.72"/>
|
||||
<PackageReference Include="NUnit" Version="4.5.1"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
26
GFramework.Cqrs.Tests/GlobalUsings.cs
Normal file
26
GFramework.Cqrs.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 Microsoft.Extensions.DependencyInjection;
|
||||
global using Moq;
|
||||
global using NUnit.Compatibility;
|
||||
global using NUnit.Framework;
|
||||
56
GFramework.Cqrs.Tests/Logging/TestLogger.cs
Normal file
56
GFramework.Cqrs.Tests/Logging/TestLogger.cs
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Logging;
|
||||
|
||||
namespace GFramework.Core.Tests.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// 供 CQRS 测试项目复用的最小日志记录器实现。
|
||||
/// </summary>
|
||||
public sealed class TestLogger : AbstractLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化测试日志记录器。
|
||||
/// </summary>
|
||||
/// <param name="name">日志名称。</param>
|
||||
/// <param name="minLevel">最小日志级别。</param>
|
||||
public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base(name, minLevel)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前测试期间捕获到的日志条目。
|
||||
/// </summary>
|
||||
public List<LogEntry> Logs { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 将日志写入内存,供断言使用。
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别。</param>
|
||||
/// <param name="message">日志消息。</param>
|
||||
/// <param name="exception">关联异常。</param>
|
||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||
{
|
||||
Logs.Add(new LogEntry(level, message, exception));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示单条测试日志记录。
|
||||
/// </summary>
|
||||
/// <param name="Level">日志级别。</param>
|
||||
/// <param name="Message">日志消息。</param>
|
||||
/// <param name="Exception">关联异常。</param>
|
||||
public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception);
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Ioc;
|
||||
@ -1,5 +1,3 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Architectures;
|
||||
@ -1,6 +1,3 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Events;
|
||||
@ -63,6 +63,9 @@
|
||||
<None Remove="Godot\**"/>
|
||||
<None Remove="GFramework.Game.Tests\**"/>
|
||||
<None Remove="GFramework.Godot.Tests\**"/>
|
||||
<None Remove="GFramework.Cqrs\**"/>
|
||||
<None Remove="GFramework.Cqrs.Abstractions\**"/>
|
||||
<None Remove="GFramework.Cqrs.Tests\**"/>
|
||||
</ItemGroup>
|
||||
<!-- 聚合核心模块 -->
|
||||
<ItemGroup>
|
||||
@ -104,6 +107,9 @@
|
||||
<Compile Remove="Godot\**"/>
|
||||
<Compile Remove="GFramework.Game.Tests\**"/>
|
||||
<Compile Remove="GFramework.Godot.Tests\**"/>
|
||||
<Compile Remove="GFramework.Cqrs\**"/>
|
||||
<Compile Remove="GFramework.Cqrs.Abstractions\**"/>
|
||||
<Compile Remove="GFramework.Cqrs.Tests\**"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="GFramework.Core\**"/>
|
||||
@ -131,6 +137,9 @@
|
||||
<EmbeddedResource Remove="Godot\**"/>
|
||||
<EmbeddedResource Remove="GFramework.Game.Tests\**"/>
|
||||
<EmbeddedResource Remove="GFramework.Godot.Tests\**"/>
|
||||
<EmbeddedResource Remove="GFramework.Cqrs\**"/>
|
||||
<EmbeddedResource Remove="GFramework.Cqrs.Abstractions\**"/>
|
||||
<EmbeddedResource Remove="GFramework.Cqrs.Tests\**"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
|
||||
|
||||
@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstraction
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -304,6 +306,18 @@ Global
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user