docs(core): 添加 CQRS 架构模式完整文档

- 新增 CQRS 核心概念介绍,包括命令、查询、处理器和分发器
- 添加基本用法示例,展示命令和查询的定义与发送流程
- 实现高级功能文档,涵盖请求、通知、管道行为和流式处理
- 提供最佳实践指南,明确命令查询分离和验证行为使用方式
- 增加常见问题解答,解释 Command/Query 区别和错误处理方案
- 新增 CQRS 处理器自动注册实现,支持源码生成和反射扫描
- 添加单元测试验证处理器注册顺序和容错行为
- 更新项目 AI 代理说明文档,完善模块依赖关系图
This commit is contained in:
GeWuYou 2026-04-15 10:21:20 +08:00
parent 295496e90f
commit fd64423741
8 changed files with 746 additions and 3 deletions

View File

@ -105,6 +105,7 @@ major 版本中移除。
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
- `CqrsHandlerRegistryGenerator`: 为消费端程序集生成 CQRS handler 注册器,运行时优先使用生成产物,无法覆盖时回退到反射扫描。
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。

View File

@ -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));
}

View 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);
}

View File

@ -115,6 +115,80 @@ internal sealed class CqrsHandlerRegistrarTests
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>
@ -219,3 +293,48 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
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}.");
}
}

View File

@ -7,7 +7,8 @@ namespace GFramework.Core.Cqrs.Internal;
/// <summary>
/// 在架构初始化期间扫描并注册 CQRS 处理器。
/// 首批实现采用运行时反射扫描,优先满足“无需额外注册步骤即可工作”的迁移目标。
/// 运行时会优先尝试使用源码生成的程序集级注册器,以减少冷启动阶段的反射开销;
/// 当目标程序集没有生成注册器,或注册器不可用时,再回退到运行时反射扫描。
/// </summary>
internal static class CqrsHandlerRegistrar
{
@ -31,10 +32,84 @@ internal static class CqrsHandlerRegistrar
.Distinct()
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
{
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
continue;
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>
/// 注册单个程序集里的所有 CQRS 处理器映射。
/// </summary>

View File

@ -0,0 +1,195 @@
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();
}
}

View File

@ -0,0 +1,311 @@
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)
{
context.RegisterSourceOutput(
context.CompilationProvider,
static (productionContext, compilation) => Execute(productionContext, compilation));
}
private static void Execute(SourceProductionContext context, Compilation compilation)
{
var requestHandlerType = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName);
var notificationHandlerType = compilation.GetTypeByMetadataName(INotificationHandlerMetadataName);
var streamHandlerType = compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName);
var registryInterfaceType = compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName);
var registryAttributeType = compilation.GetTypeByMetadataName(CqrsHandlerRegistryAttributeMetadataName);
var loggerType = compilation.GetTypeByMetadataName(ILoggerMetadataName);
var serviceCollectionType = compilation.GetTypeByMetadataName(IServiceCollectionMetadataName);
if (requestHandlerType is null ||
notificationHandlerType is null ||
streamHandlerType is null ||
registryInterfaceType is null ||
registryAttributeType is null ||
loggerType is null ||
serviceCollectionType is null)
{
return;
}
var registrations = CollectRegistrations(
compilation.Assembly.GlobalNamespace,
requestHandlerType,
notificationHandlerType,
streamHandlerType,
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(
INamespaceSymbol rootNamespace,
INamedTypeSymbol requestHandlerType,
INamedTypeSymbol notificationHandlerType,
INamedTypeSymbol streamHandlerType,
out bool hasUnsupportedConcreteHandler)
{
var registrations = new List<HandlerRegistrationSpec>();
hasUnsupportedConcreteHandler = false;
foreach (var type in EnumerateTypes(rootNamespace))
{
if (!IsConcreteHandlerType(type))
continue;
var handlerInterfaces = type.AllInterfaces
.Where(interfaceType => IsSupportedHandlerInterface(
interfaceType,
requestHandlerType,
notificationHandlerType,
streamHandlerType))
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToList();
if (handlerInterfaces.Count == 0)
continue;
if (!CanReferenceFromGeneratedRegistry(type) ||
handlerInterfaces.Any(interfaceType => !CanReferenceFromGeneratedRegistry(interfaceType)))
{
hasUnsupportedConcreteHandler = true;
return [];
}
var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var implementationLogName = GetLogDisplayName(type);
foreach (var handlerInterface in handlerInterfaces)
{
registrations.Add(new HandlerRegistrationSpec(
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
implementationTypeDisplayName,
GetLogDisplayName(handlerInterface),
implementationLogName));
}
}
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 IEnumerable<INamedTypeSymbol> EnumerateTypes(INamespaceSymbol namespaceSymbol)
{
foreach (var member in namespaceSymbol.GetMembers())
{
switch (member)
{
case INamespaceSymbol childNamespace:
foreach (var type in EnumerateTypes(childNamespace))
yield return type;
break;
case INamedTypeSymbol namedType:
foreach (var type in EnumerateTypes(namedType))
yield return type;
break;
}
}
}
private static IEnumerable<INamedTypeSymbol> EnumerateTypes(INamedTypeSymbol typeSymbol)
{
yield return typeSymbol;
foreach (var nestedType in typeSymbol.GetTypeMembers())
{
foreach (var descendant in EnumerateTypes(nestedType))
yield return descendant;
}
}
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,
INamedTypeSymbol requestHandlerType,
INamedTypeSymbol notificationHandlerType,
INamedTypeSymbol streamHandlerType)
{
if (!interfaceType.IsGenericType)
return false;
var definition = interfaceType.OriginalDefinition;
return SymbolEqualityComparer.Default.Equals(definition, requestHandlerType) ||
SymbolEqualityComparer.Default.Equals(definition, notificationHandlerType) ||
SymbolEqualityComparer.Default.Equals(definition, streamHandlerType);
}
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 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("\"", "\\\"");
}
private readonly record struct HandlerRegistrationSpec(
string HandlerInterfaceDisplayName,
string ImplementationTypeDisplayName,
string HandlerInterfaceLogName,
string ImplementationLogName);
}

View File

@ -204,7 +204,7 @@ public async Task<List<ScoreData>> GetHighScores()
### 注册处理器
在架构中注册 CQRS 行为;默认会自动扫描当前架构所在程序集和 `GFramework.Core` 程序集中的处理器:
在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器:
```csharp
public class GameArchitecture : Architecture
@ -220,7 +220,11 @@ public class GameArchitecture : Architecture
}
```
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是依赖默认扫描。
当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器;
如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。
`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。
如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围。
`RegisterCqrsPipelineBehavior<TBehavior>()` 是推荐入口;旧的 `RegisterMediatorBehavior<TBehavior>()`
仅作为兼容名称保留,当前已标记为 `Obsolete` 并从 IntelliSense 主路径隐藏,计划在未来 major 版本中移除。