mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 06:04:30 +08:00
Merge pull request #222 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-3
Refactor/Deprecate Mediator alias and introduce source-generated CQRS handler registry
This commit is contained in:
commit
f59e8f7a1f
@ -74,7 +74,8 @@ Architecture 负责统一生命周期编排,核心阶段包括:
|
|||||||
### CQRS
|
### CQRS
|
||||||
|
|
||||||
命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史
|
命令与查询分离,支持同步与异步执行。当前版本内建自有 CQRS runtime、行为管道和 handler 自动注册;公开 API 里仍保留少量历史
|
||||||
`Mediator` 命名以兼容旧调用点。
|
`Mediator` 命名以兼容旧调用点,但这些别名已进入正式弃用周期:新代码应使用 `Cqrs` 命名入口,旧别名会继续兼容一段时间并计划在未来
|
||||||
|
major 版本中移除。
|
||||||
|
|
||||||
### EventBus
|
### EventBus
|
||||||
|
|
||||||
@ -104,6 +105,7 @@ Architecture 负责统一生命周期编排,核心阶段包括:
|
|||||||
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
|
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
|
||||||
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
|
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
|
||||||
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
|
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
|
||||||
|
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。
|
||||||
|
|
||||||
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。
|
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Lifecycle;
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
using GFramework.Core.Abstractions.Model;
|
using GFramework.Core.Abstractions.Model;
|
||||||
using GFramework.Core.Abstractions.Systems;
|
using GFramework.Core.Abstractions.Systems;
|
||||||
@ -84,11 +85,14 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册 CQRS 请求管道行为。
|
/// 注册 CQRS 请求管道行为。
|
||||||
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
||||||
|
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
|
||||||
/// 既支持实现 <c>IPipelineBehavior<,></c> 的开放泛型行为类型,
|
/// 既支持实现 <c>IPipelineBehavior<,></c> 的开放泛型行为类型,
|
||||||
/// 也支持绑定到单一请求/响应对的封闭行为类型。
|
/// 也支持绑定到单一请求/响应对的封闭行为类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
|
||||||
void RegisterMediatorBehavior<TBehavior>()
|
void RegisterMediatorBehavior<TBehavior>()
|
||||||
where TBehavior : class;
|
where TBehavior : class;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
namespace GFramework.Core.Abstractions.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 声明程序集内可供运行时直接调用的 CQRS 处理器注册器类型。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该特性通常由源码生成器自动添加到消费端程序集。
|
||||||
|
/// 运行时读取到该特性后,会优先实例化对应的 <see cref="ICqrsHandlerRegistry" />,
|
||||||
|
/// 以常量时间获取处理器注册映射,而不是遍历程序集中的全部类型。
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||||
|
public sealed class CqrsHandlerRegistryAttribute(Type registryType) : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取承载 CQRS 处理器注册逻辑的注册器类型。
|
||||||
|
/// </summary>
|
||||||
|
public Type RegistryType { get; } = registryType ?? throw new ArgumentNullException(nameof(registryType));
|
||||||
|
}
|
||||||
20
GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs
Normal file
20
GFramework.Core.Abstractions/Cqrs/ICqrsHandlerRegistry.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义由源码生成器产出的 CQRS 处理器注册器契约。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 运行时会优先调用实现该接口的程序集级注册器,以避免在冷启动阶段对整个程序集执行反射扫描。
|
||||||
|
/// 当目标程序集没有生成注册器,或生成注册器因兼容性原因不可用时,运行时仍会回退到反射扫描路径。
|
||||||
|
/// </remarks>
|
||||||
|
public interface ICqrsHandlerRegistry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将当前程序集中的 CQRS 处理器映射注册到目标服务集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||||
|
/// <param name="logger">用于记录注册诊断信息的日志器。</param>
|
||||||
|
void Register(IServiceCollection services, ILogger logger);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using GFramework.Core.Abstractions.Rule;
|
using System.ComponentModel;
|
||||||
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Abstractions.Systems;
|
using GFramework.Core.Abstractions.Systems;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -99,9 +100,12 @@ public interface IIocContainer : IContextAware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册 CQRS 请求管道行为。
|
/// 注册 CQRS 请求管道行为。
|
||||||
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
||||||
|
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
|
||||||
void RegisterMediatorBehavior<TBehavior>()
|
void RegisterMediatorBehavior<TBehavior>()
|
||||||
where TBehavior : class;
|
where TBehavior : class;
|
||||||
|
|
||||||
|
|||||||
@ -115,6 +115,80 @@ internal sealed class CqrsHandlerRegistrarTests
|
|||||||
LoggerFactoryResolver.Provider = originalProvider;
|
LoggerFactoryResolver.Provider = originalProvider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当程序集提供源码生成的注册器时,运行时会优先使用该注册器而不是反射扫描类型列表。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterHandlers_Should_Use_Generated_Registry_When_Available()
|
||||||
|
{
|
||||||
|
var generatedAssembly = new Mock<Assembly>();
|
||||||
|
generatedAssembly
|
||||||
|
.SetupGet(static assembly => assembly.FullName)
|
||||||
|
.Returns("GFramework.Core.Tests.Cqrs.GeneratedRegistryAssembly, Version=1.0.0.0");
|
||||||
|
generatedAssembly
|
||||||
|
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||||
|
.Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedNotificationHandlerRegistry))]);
|
||||||
|
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var handlers = container.GetAll<INotificationHandler<GeneratedRegistryNotification>>();
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
handlers.Select(static handler => handler.GetType()),
|
||||||
|
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Invalid()
|
||||||
|
{
|
||||||
|
var originalProvider = LoggerFactoryResolver.Provider;
|
||||||
|
var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
|
||||||
|
var generatedAssembly = new Mock<Assembly>();
|
||||||
|
generatedAssembly
|
||||||
|
.SetupGet(static assembly => assembly.FullName)
|
||||||
|
.Returns("GFramework.Core.Tests.Cqrs.InvalidGeneratedRegistryAssembly, Version=1.0.0.0");
|
||||||
|
generatedAssembly
|
||||||
|
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||||
|
.Returns([new CqrsHandlerRegistryAttribute(typeof(string))]);
|
||||||
|
generatedAssembly
|
||||||
|
.Setup(static assembly => assembly.GetTypes())
|
||||||
|
.Returns([typeof(AlphaDeterministicNotificationHandler)]);
|
||||||
|
|
||||||
|
LoggerFactoryResolver.Provider = capturingProvider;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var handlers = container.GetAll<INotificationHandler<DeterministicOrderNotification>>();
|
||||||
|
var warningLogs = capturingProvider.Loggers
|
||||||
|
.SelectMany(static logger => logger.Logs)
|
||||||
|
.Where(static log => log.Level == LogLevel.Warning)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
handlers.Select(static handler => handler.GetType()),
|
||||||
|
Is.EqualTo([typeof(AlphaDeterministicNotificationHandler)]));
|
||||||
|
Assert.That(
|
||||||
|
warningLogs.Any(log =>
|
||||||
|
log.Message.Contains("does not implement", StringComparison.Ordinal)),
|
||||||
|
Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LoggerFactoryResolver.Provider = originalProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -219,3 +293,48 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
|
|||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于验证生成注册器路径的通知消息。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed record GeneratedRegistryNotification : INotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 由模拟的源码生成注册器显式注册的通知处理器。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GeneratedRegistryNotificationHandler : INotificationHandler<GeneratedRegistryNotification>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 处理生成注册器测试中的通知。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification">通知实例。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>已完成任务。</returns>
|
||||||
|
public ValueTask Handle(GeneratedRegistryNotification notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟源码生成器为某个程序集生成的 CQRS 处理器注册器。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将测试通知处理器注册到目标服务集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||||
|
/// <param name="logger">用于记录注册诊断的日志器。</param>
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(INotificationHandler<GeneratedRegistryNotification>),
|
||||||
|
typeof(GeneratedRegistryNotificationHandler));
|
||||||
|
logger.Debug(
|
||||||
|
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,99 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
|
using GFramework.Core.Architectures;
|
||||||
|
using GFramework.Core.Coroutine.Extensions;
|
||||||
|
using GFramework.Core.Ioc;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 锁定历史 Mediator 兼容入口的正式弃用策略。
|
||||||
|
/// 这些测试确保旧 API 不仅保留行为兼容,还会通过编译期提示和 IntelliSense 隐藏引导调用方迁移到新的 CQRS 命名。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class MediatorCompatibilityDeprecationTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证公开兼容方法仍可用,但已被显式标记为未来移除的旧别名。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Legacy_Public_Methods_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing()
|
||||||
|
{
|
||||||
|
AssertLegacyMethod(typeof(IArchitecture), nameof(IArchitecture.RegisterMediatorBehavior));
|
||||||
|
AssertLegacyMethod(typeof(IIocContainer), nameof(IIocContainer.RegisterMediatorBehavior));
|
||||||
|
AssertLegacyMethod(typeof(Architecture), nameof(Architecture.RegisterMediatorBehavior));
|
||||||
|
AssertLegacyMethod(typeof(MicrosoftDiContainer), nameof(MicrosoftDiContainer.RegisterMediatorBehavior));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证历史扩展类型会把迁移目标写入弃用说明,并从 IntelliSense 主路径隐藏。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Legacy_Extension_Types_Should_Be_Obsolete_And_Hidden_From_Editor_Browsing()
|
||||||
|
{
|
||||||
|
AssertLegacyType(
|
||||||
|
typeof(ContextAwareMediatorExtensions),
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead.");
|
||||||
|
AssertLegacyType(
|
||||||
|
typeof(ContextAwareMediatorCommandExtensions),
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.");
|
||||||
|
AssertLegacyType(
|
||||||
|
typeof(ContextAwareMediatorQueryExtensions),
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.");
|
||||||
|
AssertLegacyType(
|
||||||
|
typeof(MediatorCoroutineExtensions),
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 断言方法级兼容 API 具备统一的弃用元数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="declaringType">声明该方法的类型。</param>
|
||||||
|
/// <param name="methodName">方法名称。</param>
|
||||||
|
private static void AssertLegacyMethod(Type declaringType, string methodName)
|
||||||
|
{
|
||||||
|
var method = declaringType
|
||||||
|
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Single(candidate => candidate.Name == methodName);
|
||||||
|
|
||||||
|
var obsoleteAttribute = method.GetCustomAttribute<ObsoleteAttribute>();
|
||||||
|
var editorBrowsableAttribute = method.GetCustomAttribute<EditorBrowsableAttribute>();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(obsoleteAttribute, Is.Not.Null);
|
||||||
|
Assert.That(
|
||||||
|
obsoleteAttribute!.Message,
|
||||||
|
Does.Contain("Use RegisterCqrsPipelineBehavior<TBehavior>() instead."));
|
||||||
|
Assert.That(
|
||||||
|
obsoleteAttribute.Message,
|
||||||
|
Does.Contain("removed in a future major version"));
|
||||||
|
Assert.That(editorBrowsableAttribute, Is.Not.Null);
|
||||||
|
Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 断言类型级兼容扩展具备统一的弃用元数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">兼容扩展类型。</param>
|
||||||
|
/// <param name="expectedReplacementHint">期望的迁移提示。</param>
|
||||||
|
private static void AssertLegacyType(Type type, string expectedReplacementHint)
|
||||||
|
{
|
||||||
|
var obsoleteAttribute = type.GetCustomAttribute<ObsoleteAttribute>();
|
||||||
|
var editorBrowsableAttribute = type.GetCustomAttribute<EditorBrowsableAttribute>();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(obsoleteAttribute, Is.Not.Null);
|
||||||
|
Assert.That(obsoleteAttribute!.Message, Does.Contain(expectedReplacementHint));
|
||||||
|
Assert.That(
|
||||||
|
obsoleteAttribute.Message,
|
||||||
|
Does.Contain("removed in a future major version"));
|
||||||
|
Assert.That(editorBrowsableAttribute, Is.Not.Null);
|
||||||
|
Assert.That(editorBrowsableAttribute!.State, Is.EqualTo(EditorBrowsableState.Never));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
@ -157,9 +158,12 @@ public abstract class Architecture : IArchitecture
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册 CQRS 请求管道行为。
|
/// 注册 CQRS 请求管道行为。
|
||||||
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
||||||
|
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||||
{
|
{
|
||||||
RegisterCqrsPipelineBehavior<TBehavior>();
|
RegisterCqrsPipelineBehavior<TBehavior>();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
@ -26,9 +27,12 @@ internal sealed class ArchitectureModules(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册 CQRS 请求管道行为。
|
/// 注册 CQRS 请求管道行为。
|
||||||
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
||||||
|
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||||
{
|
{
|
||||||
RegisterCqrsPipelineBehavior<TBehavior>();
|
RegisterCqrsPipelineBehavior<TBehavior>();
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Coroutine;
|
using GFramework.Core.Abstractions.Coroutine;
|
||||||
using GFramework.Core.Abstractions.Cqrs;
|
using GFramework.Core.Abstractions.Cqrs;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
@ -21,8 +22,11 @@ namespace GFramework.Core.Coroutine.Extensions;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供 CQRS 命令与协程集成的扩展方法。
|
/// 提供 CQRS 命令与协程集成的扩展方法。
|
||||||
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions" />。
|
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions" />。
|
||||||
|
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.CqrsCoroutineExtensions instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public static class MediatorCoroutineExtensions
|
public static class MediatorCoroutineExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -7,7 +7,8 @@ namespace GFramework.Core.Cqrs.Internal;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在架构初始化期间扫描并注册 CQRS 处理器。
|
/// 在架构初始化期间扫描并注册 CQRS 处理器。
|
||||||
/// 首批实现采用运行时反射扫描,优先满足“无需额外注册步骤即可工作”的迁移目标。
|
/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销;
|
||||||
|
/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class CqrsHandlerRegistrar
|
internal static class CqrsHandlerRegistrar
|
||||||
{
|
{
|
||||||
@ -31,10 +32,84 @@ internal static class CqrsHandlerRegistrar
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
|
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
|
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
|
||||||
|
continue;
|
||||||
|
|
||||||
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">目标服务集合。</param>
|
||||||
|
/// <param name="assembly">当前要处理的程序集。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
/// <returns>当成功使用生成注册器时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
|
private static bool TryRegisterGeneratedHandlers(IServiceCollection services, Assembly assembly, ILogger logger)
|
||||||
|
{
|
||||||
|
var assemblyName = GetAssemblySortKey(assembly);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var registryTypes = assembly
|
||||||
|
.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), inherit: false)
|
||||||
|
.OfType<CqrsHandlerRegistryAttribute>()
|
||||||
|
.Select(static attribute => attribute.RegistryType)
|
||||||
|
.Where(static type => type is not null)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (registryTypes.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var registries = new List<ICqrsHandlerRegistry>(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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registryType.IsAbstract)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册单个程序集里的所有 CQRS 处理器映射。
|
/// 注册单个程序集里的所有 CQRS 处理器映射。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Cqrs.Command;
|
using GFramework.Core.Abstractions.Cqrs.Command;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Cqrs.Extensions;
|
using GFramework.Core.Cqrs.Extensions;
|
||||||
@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。
|
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 命令扩展方法。
|
||||||
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions" />。
|
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions" />。
|
||||||
|
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsCommandExtensions instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public static class ContextAwareMediatorCommandExtensions
|
public static class ContextAwareMediatorCommandExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Cqrs;
|
using GFramework.Core.Abstractions.Cqrs;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Cqrs.Extensions;
|
using GFramework.Core.Cqrs.Extensions;
|
||||||
@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一接口扩展方法。
|
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 统一接口扩展方法。
|
||||||
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions" />。
|
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions" />。
|
||||||
|
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsExtensions instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public static class ContextAwareMediatorExtensions
|
public static class ContextAwareMediatorExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Cqrs.Query;
|
using GFramework.Core.Abstractions.Cqrs.Query;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Cqrs.Extensions;
|
using GFramework.Core.Cqrs.Extensions;
|
||||||
@ -7,8 +8,11 @@ namespace GFramework.Core.Extensions;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。
|
/// 提供对 <see cref="IContextAware" /> 接口的 CQRS 查询扩展方法。
|
||||||
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions" />。
|
/// 该类型保留旧名称以兼容历史调用点;新代码应改用 <see cref="GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions" />。
|
||||||
|
/// 兼容层计划在未来的 major 版本中移除,因此不会继续承载新能力。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use GFramework.Core.Cqrs.Extensions.ContextAwareCqrsQueryExtensions instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public static class ContextAwareMediatorQueryExtensions
|
public static class ContextAwareMediatorQueryExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using GFramework.Core.Abstractions.Bases;
|
using GFramework.Core.Abstractions.Bases;
|
||||||
using GFramework.Core.Abstractions.Cqrs;
|
using GFramework.Core.Abstractions.Cqrs;
|
||||||
using GFramework.Core.Abstractions.Ioc;
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
@ -360,9 +361,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册 CQRS 请求管道行为。
|
/// 注册 CQRS 请求管道行为。
|
||||||
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
/// 该成员保留旧名称以兼容历史调用点,内部行为与 <see cref="RegisterCqrsPipelineBehavior{TBehavior}" /> 一致。
|
||||||
|
/// 新代码不应继续依赖该别名;兼容层计划在未来的 major 版本中移除。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||||
[Obsolete("Use RegisterCqrsPipelineBehavior<TBehavior>() instead.")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
[Obsolete(
|
||||||
|
"Use RegisterCqrsPipelineBehavior<TBehavior>() instead. This compatibility alias will be removed in a future major version.")]
|
||||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||||
{
|
{
|
||||||
RegisterCqrsPipelineBehavior<TBehavior>();
|
RegisterCqrsPipelineBehavior<TBehavior>();
|
||||||
|
|||||||
@ -0,0 +1,215 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using GFramework.SourceGenerators.Cqrs;
|
||||||
|
using GFramework.SourceGenerators.Tests.Core;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Tests.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 CQRS 处理器注册生成器的输出与回退边界。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class CqrsHandlerRegistryGeneratorTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
|
||||||
|
/// </summary>
|
||||||
|
[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.Core.Abstractions.Cqrs
|
||||||
|
{
|
||||||
|
public interface IRequest<TResponse> { }
|
||||||
|
public interface INotification { }
|
||||||
|
public interface IStreamRequest<TResponse> { }
|
||||||
|
|
||||||
|
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||||
|
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||||
|
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||||
|
|
||||||
|
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.Core.Abstractions.Cqrs;
|
||||||
|
|
||||||
|
public sealed record PingQuery() : IRequest<string>;
|
||||||
|
public sealed record DomainEvent() : INotification;
|
||||||
|
public sealed record NumberStream() : IStreamRequest<int>;
|
||||||
|
|
||||||
|
public sealed class ZetaNotificationHandler : INotificationHandler<DomainEvent> { }
|
||||||
|
public sealed class AlphaQueryHandler : IRequestHandler<PingQuery, string> { }
|
||||||
|
public sealed class StreamHandler : IStreamRequestHandler<NumberStream, int> { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string expected = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
[assembly: global::GFramework.Core.Abstractions.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]
|
||||||
|
|
||||||
|
namespace GFramework.Generated.Cqrs;
|
||||||
|
|
||||||
|
internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Core.Abstractions.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.Core.Abstractions.Cqrs.IRequestHandler<global::TestApp.PingQuery, string>),
|
||||||
|
typeof(global::TestApp.AlphaQueryHandler));
|
||||||
|
logger.Debug("Registered CQRS handler TestApp.AlphaQueryHandler as GFramework.Core.Abstractions.Cqrs.IRequestHandler<TestApp.PingQuery, string>.");
|
||||||
|
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
|
||||||
|
services,
|
||||||
|
typeof(global::GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler<global::TestApp.NumberStream, int>),
|
||||||
|
typeof(global::TestApp.StreamHandler));
|
||||||
|
logger.Debug("Registered CQRS handler TestApp.StreamHandler as GFramework.Core.Abstractions.Cqrs.IStreamRequestHandler<TestApp.NumberStream, int>.");
|
||||||
|
global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
|
||||||
|
services,
|
||||||
|
typeof(global::GFramework.Core.Abstractions.Cqrs.INotificationHandler<global::TestApp.DomainEvent>),
|
||||||
|
typeof(global::TestApp.ZetaNotificationHandler));
|
||||||
|
logger.Debug("Registered CQRS handler TestApp.ZetaNotificationHandler as GFramework.Core.Abstractions.Cqrs.INotificationHandler<TestApp.DomainEvent>.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("CqrsHandlerRegistry.g.cs", expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_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.Core.Abstractions.Cqrs
|
||||||
|
{
|
||||||
|
public interface IRequest<TResponse> { }
|
||||||
|
public interface INotification { }
|
||||||
|
public interface IStreamRequest<TResponse> { }
|
||||||
|
|
||||||
|
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||||
|
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||||
|
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||||
|
|
||||||
|
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.Core.Abstractions.Cqrs;
|
||||||
|
|
||||||
|
public sealed record VisibleRequest() : IRequest<string>;
|
||||||
|
|
||||||
|
public sealed class Container
|
||||||
|
{
|
||||||
|
private sealed record HiddenRequest() : IRequest<string>;
|
||||||
|
|
||||||
|
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, string> { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string> { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var test = new CSharpSourceGeneratorTest<CqrsHandlerRegistryGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
TestState =
|
||||||
|
{
|
||||||
|
Sources = { source }
|
||||||
|
},
|
||||||
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await test.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
|
||||||
|
/// </summary>
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
398
GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
Normal file
398
GFramework.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Cqrs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||||
|
/// </summary>
|
||||||
|
[Generator]
|
||||||
|
public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
private const string CqrsNamespace = $"{PathContests.CoreAbstractionsNamespace}.Cqrs";
|
||||||
|
private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging";
|
||||||
|
private const string IRequestHandlerMetadataName = $"{CqrsNamespace}.IRequestHandler`2";
|
||||||
|
private const string INotificationHandlerMetadataName = $"{CqrsNamespace}.INotificationHandler`1";
|
||||||
|
private const string IStreamRequestHandlerMetadataName = $"{CqrsNamespace}.IStreamRequestHandler`2";
|
||||||
|
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsNamespace}.ICqrsHandlerRegistry";
|
||||||
|
private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsNamespace}.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";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
var generationEnabled = context.CompilationProvider
|
||||||
|
.Select(static (compilation, _) => HasRequiredTypes(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(
|
||||||
|
generationEnabled.Combine(handlerCandidates),
|
||||||
|
static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasRequiredTypes(Compilation compilation)
|
||||||
|
{
|
||||||
|
return 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (!CanReferenceFromGeneratedRegistry(type) ||
|
||||||
|
handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType)))
|
||||||
|
{
|
||||||
|
return new HandlerCandidateAnalysis(
|
||||||
|
implementationTypeDisplayName,
|
||||||
|
ImmutableArray<HandlerRegistrationSpec>.Empty,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var implementationLogName = GetLogDisplayName(type);
|
||||||
|
var registrations = ImmutableArray.CreateBuilder<HandlerRegistrationSpec>(handlerInterfaces.Length);
|
||||||
|
foreach (var handlerInterface in handlerInterfaces)
|
||||||
|
{
|
||||||
|
registrations.Add(new HandlerRegistrationSpec(
|
||||||
|
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||||
|
implementationTypeDisplayName,
|
||||||
|
GetLogDisplayName(handlerInterface),
|
||||||
|
implementationLogName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HandlerCandidateAnalysis(
|
||||||
|
implementationTypeDisplayName,
|
||||||
|
registrations.MoveToImmutable(),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Execute(SourceProductionContext context, bool generationEnabled,
|
||||||
|
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
||||||
|
{
|
||||||
|
if (!generationEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var registrations = CollectRegistrations(candidates, out var hasUnsupportedConcreteHandler);
|
||||||
|
|
||||||
|
// If the assembly contains handlers that generated code cannot legally reference
|
||||||
|
// (for example private nested handlers), keep the runtime on the reflection path
|
||||||
|
// so registration behavior remains complete instead of silently dropping handlers.
|
||||||
|
if (hasUnsupportedConcreteHandler || registrations.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.AddSource(HintName, GenerateSource(registrations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HandlerRegistrationSpec> CollectRegistrations(
|
||||||
|
ImmutableArray<HandlerCandidateAnalysis?> candidates,
|
||||||
|
out bool hasUnsupportedConcreteHandler)
|
||||||
|
{
|
||||||
|
var registrations = new List<HandlerRegistrationSpec>();
|
||||||
|
hasUnsupportedConcreteHandler = false;
|
||||||
|
|
||||||
|
// Partial declarations surface the same symbol through multiple syntax nodes.
|
||||||
|
// Collapse them by implementation type so generated registrations stay stable and duplicate-free.
|
||||||
|
var uniqueCandidates = new Dictionary<string, HandlerCandidateAnalysis>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (candidate is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (candidate.Value.HasUnsupportedConcreteHandler)
|
||||||
|
{
|
||||||
|
hasUnsupportedConcreteHandler = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var candidate in uniqueCandidates.Values)
|
||||||
|
{
|
||||||
|
registrations.AddRange(candidate.Registrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
registrations.Sort(static (left, right) =>
|
||||||
|
{
|
||||||
|
var implementationComparison = StringComparer.Ordinal.Compare(
|
||||||
|
left.ImplementationLogName,
|
||||||
|
right.ImplementationLogName);
|
||||||
|
|
||||||
|
return implementationComparison != 0
|
||||||
|
? implementationComparison
|
||||||
|
: StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanReferenceFromGeneratedRegistry(ITypeSymbol type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case IArrayTypeSymbol arrayType:
|
||||||
|
return CanReferenceFromGeneratedRegistry(arrayType.ElementType);
|
||||||
|
case INamedTypeSymbol namedType:
|
||||||
|
if (!IsTypeChainAccessible(namedType))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return namedType.TypeArguments.All(CanReferenceFromGeneratedRegistry);
|
||||||
|
case IPointerTypeSymbol pointerType:
|
||||||
|
return CanReferenceFromGeneratedRegistry(pointerType.PointedAtType);
|
||||||
|
case ITypeParameterSymbol:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTypeChainAccessible(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
for (var current = type; current is not null; current = current.ContainingType)
|
||||||
|
{
|
||||||
|
if (!IsSymbolAccessible(current))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSymbolAccessible(ISymbol symbol)
|
||||||
|
{
|
||||||
|
return symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal
|
||||||
|
or Accessibility.ProtectedOrInternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
var nestedTypes = new Stack<string>();
|
||||||
|
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 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<HandlerRegistrationSpec> registrations)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.Append("[assembly: global::");
|
||||||
|
builder.Append(CqrsNamespace);
|
||||||
|
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(CqrsNamespace);
|
||||||
|
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));");
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
foreach (var registration in registrations)
|
||||||
|
{
|
||||||
|
builder.AppendLine(
|
||||||
|
" global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(");
|
||||||
|
builder.AppendLine(" services,");
|
||||||
|
builder.Append(" typeof(");
|
||||||
|
builder.Append(registration.HandlerInterfaceDisplayName);
|
||||||
|
builder.AppendLine("),");
|
||||||
|
builder.Append(" typeof(");
|
||||||
|
builder.Append(registration.ImplementationTypeDisplayName);
|
||||||
|
builder.AppendLine("));");
|
||||||
|
builder.Append(" logger.Debug(\"Registered CQRS handler ");
|
||||||
|
builder.Append(EscapeStringLiteral(registration.ImplementationLogName));
|
||||||
|
builder.Append(" as ");
|
||||||
|
builder.Append(EscapeStringLiteral(registration.HandlerInterfaceLogName));
|
||||||
|
builder.AppendLine(".\");");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
|
||||||
|
{
|
||||||
|
public HandlerCandidateAnalysis(
|
||||||
|
string implementationTypeDisplayName,
|
||||||
|
ImmutableArray<HandlerRegistrationSpec> registrations,
|
||||||
|
bool hasUnsupportedConcreteHandler)
|
||||||
|
{
|
||||||
|
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
||||||
|
Registrations = registrations;
|
||||||
|
HasUnsupportedConcreteHandler = hasUnsupportedConcreteHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ImplementationTypeDisplayName { get; }
|
||||||
|
|
||||||
|
public ImmutableArray<HandlerRegistrationSpec> Registrations { get; }
|
||||||
|
|
||||||
|
public bool HasUnsupportedConcreteHandler { get; }
|
||||||
|
|
||||||
|
public bool Equals(HandlerCandidateAnalysis other)
|
||||||
|
{
|
||||||
|
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
|
||||||
|
StringComparison.Ordinal) ||
|
||||||
|
HasUnsupportedConcreteHandler != other.HasUnsupportedConcreteHandler ||
|
||||||
|
Registrations.Length != other.Registrations.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < Registrations.Length; index++)
|
||||||
|
{
|
||||||
|
if (!Registrations[index].Equals(other.Registrations[index]))
|
||||||
|
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) ^ HasUnsupportedConcreteHandler.GetHashCode();
|
||||||
|
foreach (var registration in Registrations)
|
||||||
|
{
|
||||||
|
hashCode = (hashCode * 397) ^ registration.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -204,7 +204,7 @@ public async Task<List<ScoreData>> GetHighScores()
|
|||||||
|
|
||||||
### 注册处理器
|
### 注册处理器
|
||||||
|
|
||||||
在架构中注册 CQRS 行为;默认会自动扫描当前架构所在程序集和 `GFramework.Core` 程序集中的处理器:
|
在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class GameArchitecture : Architecture
|
public class GameArchitecture : Architecture
|
||||||
@ -220,10 +220,15 @@ public class GameArchitecture : Architecture
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。
|
当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器;
|
||||||
|
如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。
|
||||||
|
`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。
|
||||||
|
|
||||||
|
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围。
|
||||||
|
|
||||||
`RegisterCqrsPipelineBehavior<TBehavior>()` 是推荐入口;旧的 `RegisterMediatorBehavior<TBehavior>()`
|
`RegisterCqrsPipelineBehavior<TBehavior>()` 是推荐入口;旧的 `RegisterMediatorBehavior<TBehavior>()`
|
||||||
仅作为兼容名称保留。当前接口支持两种形式:
|
仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。
|
||||||
|
`ContextAwareMediator*Extensions` 与 `MediatorCoroutineExtensions` 也遵循同样的弃用节奏。当前接口支持两种形式:
|
||||||
|
|
||||||
- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求
|
- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求
|
||||||
- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior`
|
- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user