Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
workflows: ["CI - Build & Test"]
types:
- completed
branches:
- main
workflow_dispatch:
concurrency:
group: auto-tag-main
Expand All @@ -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
Expand Down Expand Up @@ -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 changes: 4 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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: [ '**' ]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

permissions:
contents: read
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 * * *'

Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ major 版本中移除。
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描;非默认程序集可通过
`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 显式接入同一路径。

这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。

Expand Down
20 changes: 19 additions & 1 deletion GFramework.Core.Abstractions/Architectures/IArchitecture.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -96,6 +96,24 @@ void RegisterCqrsPipelineBehavior<TBehavior>()
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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// 安装架构模块
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion GFramework.Core.Abstractions/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
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;
21 changes: 20 additions & 1 deletion GFramework.Core.Abstractions/Ioc/IIocContainer.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -109,6 +109,25 @@ void RegisterCqrsPipelineBehavior<TBehavior>()
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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.


/// <summary>
/// 配置服务
Expand Down
Original file line number Diff line number Diff line change
@@ -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.");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// 验证显式声明的额外程序集会在初始化阶段接入当前架构容器。
/// </summary>
/// <returns>The asynchronous test task.</returns>
[Test]
public async Task RegisterCqrsHandlersFromAssembly_Should_Register_Handlers_From_Explicit_Assembly()
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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);

Comment thread
GeWuYou marked this conversation as resolved.
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;
}
}
Loading
Loading