mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
Merge pull request #226 from GeWuYou/refactor/cqrs-architecture-decoupling-todo-7
Refactor/cqrs architecture decoupling todo 7
This commit is contained in:
commit
21627c0381
@ -18,7 +18,7 @@ namespace GFramework.Core.Abstractions.Architectures;
|
||||
/// <para>新的 <c>GFramework.Cqrs.Abstractions.Cqrs</c> 契约由内置 CQRS dispatcher 统一处理,支持 request pipeline、notification publish 与 stream request。</para>
|
||||
/// <para>新功能优先使用 <see cref="SendRequestAsync{TResponse}(IRequest{TResponse},CancellationToken)" />、<see cref="SendAsync{TCommand}(TCommand,CancellationToken)" /> 与对应的 CQRS Command/Query 重载;迁移旧代码时可先保留旧入口,再逐步替换为 CQRS 请求模型。</para>
|
||||
/// </remarks>
|
||||
public interface IArchitectureContext
|
||||
public interface IArchitectureContext : ICqrsContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取指定类型的服务实例
|
||||
|
||||
@ -1,52 +1,16 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GFramework.Core.Abstractions.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 定义架构上下文使用的 CQRS runtime seam。
|
||||
/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦,
|
||||
/// 使 <see cref="IArchitectureContext" /> 不再直接依赖某个固定的 runtime 类型。
|
||||
/// 提供旧 <c>GFramework.Core.Abstractions.Cqrs</c> 命名空间下的 CQRS runtime 兼容别名。
|
||||
/// </summary>
|
||||
public interface ICqrsRuntime
|
||||
/// <remarks>
|
||||
/// 正式 runtime seam 已迁移到 <see cref="GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime" />,
|
||||
/// 但当前仍保留该接口以避免立即打断历史公开路径与既有二进制引用。
|
||||
/// 新代码应优先依赖 <c>GFramework.Cqrs.Abstractions.Cqrs</c> 下的正式契约。
|
||||
/// </remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ICqrsRuntime : GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// 发送请求并返回响应。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
/// <param name="context">当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。</param>
|
||||
/// <param name="request">要分发的请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>请求响应。</returns>
|
||||
ValueTask<TResponse> SendAsync<TResponse>(
|
||||
IArchitectureContext context,
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发布通知到所有已注册处理器。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNotification">通知类型。</typeparam>
|
||||
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="notification">要发布的通知。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示通知分发完成的值任务。</returns>
|
||||
ValueTask PublishAsync<TNotification>(
|
||||
IArchitectureContext context,
|
||||
TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 创建流式请求的异步响应序列。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流元素类型。</typeparam>
|
||||
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="request">流式请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>按需生成的异步响应序列。</returns>
|
||||
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||||
IArchitectureContext context,
|
||||
IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Bases;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Tests.Cqrs;
|
||||
using GFramework.Core.Tests.Systems;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
|
||||
|
||||
namespace GFramework.Core.Tests.Ioc;
|
||||
|
||||
@ -160,12 +160,16 @@ public class MicrosoftDiContainerTests
|
||||
public void RegisterHandlers_Should_Not_Duplicate_Cqrs_Infrastructure_When_It_Is_Already_Registered()
|
||||
{
|
||||
Assert.That(_container.GetAll<ICqrsRuntime>(), Has.Count.EqualTo(1));
|
||||
Assert.That(_container.GetAll<LegacyICqrsRuntime>(), Has.Count.EqualTo(1));
|
||||
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
|
||||
Assert.That(_container.Get<ICqrsRuntime>(), Is.SameAs(_container.Get<LegacyICqrsRuntime>()));
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(_container);
|
||||
|
||||
Assert.That(_container.GetAll<ICqrsRuntime>(), Has.Count.EqualTo(1));
|
||||
Assert.That(_container.GetAll<LegacyICqrsRuntime>(), Has.Count.EqualTo(1));
|
||||
Assert.That(_container.GetAll<ICqrsHandlerRegistrar>(), Has.Count.EqualTo(1));
|
||||
Assert.That(_container.Get<ICqrsRuntime>(), Is.SameAs(_container.Get<LegacyICqrsRuntime>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -245,6 +249,46 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(results.Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试容器未冻结时,会折叠“不同服务类型指向同一实例”的兼容别名重复,
|
||||
/// 但会保留同一服务类型的重复显式注册。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetAll_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases()
|
||||
{
|
||||
var instance = new AliasAwareService();
|
||||
|
||||
_container.Register<IPrimaryAliasService>(instance);
|
||||
_container.Register<IPrimaryAliasService>(instance);
|
||||
_container.Register<ISecondaryAliasService>(instance);
|
||||
|
||||
var results = _container.GetAll<ISharedAliasService>();
|
||||
|
||||
Assert.That(results, Has.Count.EqualTo(2));
|
||||
Assert.That(results[0], Is.SameAs(instance));
|
||||
Assert.That(results[1], Is.SameAs(instance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试非泛型 GetAll 在容器未冻结时与泛型重载保持相同的别名去重语义。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void
|
||||
GetAll_Type_Should_Preserve_Duplicate_Registrations_For_The_Same_ServiceType_While_Deduplicating_Aliases()
|
||||
{
|
||||
var instance = new AliasAwareService();
|
||||
|
||||
_container.Register<IPrimaryAliasService>(instance);
|
||||
_container.Register<IPrimaryAliasService>(instance);
|
||||
_container.Register<ISecondaryAliasService>(instance);
|
||||
|
||||
var results = _container.GetAll(typeof(ISharedAliasService));
|
||||
|
||||
Assert.That(results, Has.Count.EqualTo(2));
|
||||
Assert.That(results[0], Is.SameAs(instance));
|
||||
Assert.That(results[1], Is.SameAs(instance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试获取排序后的所有实例的功能
|
||||
/// </summary>
|
||||
@ -357,6 +401,17 @@ public class MicrosoftDiContainerTests
|
||||
Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试当程序集集合中包含空元素时,CQRS handler 注册入口会在委托给注册服务前直接失败。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterCqrsHandlersFromAssemblies_WithNullAssemblyItem_Should_ThrowArgumentNullException()
|
||||
{
|
||||
var assemblies = new Assembly[] { typeof(DeterministicOrderNotification).Assembly, null! };
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => _container.RegisterCqrsHandlersFromAssemblies(assemblies));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试冻结容器以防止进一步注册的功能
|
||||
/// </summary>
|
||||
@ -712,6 +767,28 @@ public interface IMixedService
|
||||
string? Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证未冻结查询路径中的服务别名去重行为。
|
||||
/// </summary>
|
||||
public interface ISharedAliasService;
|
||||
|
||||
/// <summary>
|
||||
/// 主服务别名接口。
|
||||
/// </summary>
|
||||
public interface IPrimaryAliasService : ISharedAliasService;
|
||||
|
||||
/// <summary>
|
||||
/// 次级兼容别名接口。
|
||||
/// </summary>
|
||||
public interface ISecondaryAliasService : ISharedAliasService;
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现多个别名接口的测试服务。
|
||||
/// </summary>
|
||||
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实现优先级的服务
|
||||
/// </summary>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Command;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
using GFramework.Core.Abstractions.Events;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
@ -190,7 +189,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <typeparam name="TResponse">查询响应类型</typeparam>
|
||||
/// <param name="query">要发送的查询对象</param>
|
||||
/// <returns>查询结果</returns>
|
||||
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
|
||||
public TResponse SendQuery<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
|
||||
{
|
||||
return SendQueryAsync(query).AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
@ -216,8 +215,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <param name="query">要发送的查询对象</param>
|
||||
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
|
||||
/// <returns>包含查询结果的ValueTask</returns>
|
||||
public async ValueTask<TResponse> SendQueryAsync<TResponse>(
|
||||
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
|
||||
public async ValueTask<TResponse> SendQueryAsync<TResponse>(Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
@ -354,7 +352,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <param name="cancellationToken">取消令牌,用于取消操作</param>
|
||||
/// <returns>包含命令执行结果的ValueTask</returns>
|
||||
public async ValueTask<TResponse> SendCommandAsync<TResponse>(
|
||||
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
|
||||
Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
@ -393,7 +391,7 @@ public class ArchitectureContext : IArchitectureContext
|
||||
/// <typeparam name="TResponse">命令响应类型</typeparam>
|
||||
/// <param name="command">要发送的命令对象</param>
|
||||
/// <returns>命令执行结果</returns>
|
||||
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
|
||||
public TResponse SendCommand<TResponse>(Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
|
||||
{
|
||||
return SendCommandAsync(command).AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ using GFramework.Core.Abstractions.Bases;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Rule;
|
||||
using GFramework.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Core.Ioc;
|
||||
@ -35,6 +35,14 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 记录某个实例在未冻结查询中可见的服务类型分组信息。
|
||||
/// </summary>
|
||||
/// <param name="ServiceType">当前分组对应的服务类型。</param>
|
||||
/// <param name="Count">该服务类型下的描述符数量。</param>
|
||||
/// <param name="FirstIndex">该服务类型首次出现的位置,用于稳定打破并列。</param>
|
||||
private sealed record VisibleServiceTypeGroup(Type ServiceType, int Count, int FirstIndex);
|
||||
|
||||
#region Fields
|
||||
|
||||
/// <summary>
|
||||
@ -57,12 +65,6 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
private readonly HashSet<object> _registeredInstances = [];
|
||||
|
||||
/// <summary>
|
||||
/// 已接入 CQRS handler 注册流程的程序集键集合。
|
||||
/// 使用稳定字符串键而不是 Assembly 引用本身,以避免默认路径和显式扩展路径使用不同 Assembly 对象时重复注册。
|
||||
/// </summary>
|
||||
private readonly HashSet<string> _registeredCqrsHandlerAssemblyKeys = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// 日志记录器,用于记录容器操作日志
|
||||
/// </summary>
|
||||
@ -397,35 +399,22 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assemblies" /> 中存在 <see langword="null" /> 元素。</exception>
|
||||
/// <exception cref="InvalidOperationException">容器已冻结,无法继续注册 CQRS 处理器。</exception>
|
||||
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
var assemblyArray = assemblies.ToArray();
|
||||
foreach (var assembly in assemblyArray)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
}
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
ThrowIfFrozen();
|
||||
|
||||
var processedAssemblyKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var assembly in assemblies
|
||||
.Where(static assembly => assembly is not null)
|
||||
.OrderBy(GetCqrsAssemblyRegistrationKey, StringComparer.Ordinal))
|
||||
{
|
||||
var assemblyKey = GetCqrsAssemblyRegistrationKey(assembly);
|
||||
if (!processedAssemblyKeys.Add(assemblyKey))
|
||||
continue;
|
||||
|
||||
if (_registeredCqrsHandlerAssemblyKeys.Contains(assemblyKey))
|
||||
{
|
||||
_logger.Debug(
|
||||
$"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered.");
|
||||
continue;
|
||||
}
|
||||
|
||||
ResolveCqrsHandlerRegistrar().RegisterHandlers([assembly]);
|
||||
_registeredCqrsHandlerAssemblyKeys.Add(assemblyKey);
|
||||
}
|
||||
ResolveCqrsRegistrationService().RegisterHandlers(assemblyArray);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -456,22 +445,22 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
#region Get
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前容器中已注册的 CQRS 处理器注册器。
|
||||
/// 获取当前容器中已注册的 CQRS 程序集注册协调器。
|
||||
/// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定,
|
||||
/// 避免在容器未冻结前依赖完整的服务提供者构建流程。
|
||||
/// </summary>
|
||||
/// <returns>已注册的 CQRS 处理器注册器实例。</returns>
|
||||
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 处理器注册器实例时抛出。</exception>
|
||||
private ICqrsHandlerRegistrar ResolveCqrsHandlerRegistrar()
|
||||
/// <returns>已注册的 CQRS 程序集注册协调器实例。</returns>
|
||||
/// <exception cref="InvalidOperationException">未找到可用的 CQRS 程序集注册协调器实例时抛出。</exception>
|
||||
private ICqrsRegistrationService ResolveCqrsRegistrationService()
|
||||
{
|
||||
var descriptor = GetServicesUnsafe.LastOrDefault(static service =>
|
||||
service.ServiceType == typeof(ICqrsHandlerRegistrar));
|
||||
service.ServiceType == typeof(ICqrsRegistrationService));
|
||||
|
||||
if (descriptor?.ImplementationInstance is ICqrsHandlerRegistrar registrar)
|
||||
return registrar;
|
||||
if (descriptor?.ImplementationInstance is ICqrsRegistrationService registrationService)
|
||||
return registrationService;
|
||||
|
||||
const string errorMessage =
|
||||
"ICqrsHandlerRegistrar not registered. Ensure the CQRS runtime module has been installed before registering handlers.";
|
||||
"ICqrsRegistrationService not registered. Ensure the CQRS runtime module has been installed before registering handlers.";
|
||||
_logger.Error(errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
@ -618,29 +607,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
{
|
||||
if (_provider == null)
|
||||
{
|
||||
// 如果容器未冻结,从服务集合中获取已注册的实例
|
||||
var serviceType = typeof(T);
|
||||
var registeredServices = GetServicesUnsafe
|
||||
.Where(s => s.ServiceType == serviceType || serviceType.IsAssignableFrom(s.ServiceType)).ToList();
|
||||
|
||||
var result = new List<T>();
|
||||
foreach (var descriptor in registeredServices)
|
||||
{
|
||||
if (descriptor.ImplementationInstance is T instance)
|
||||
{
|
||||
result.Add(instance);
|
||||
}
|
||||
else if (descriptor.ImplementationFactory != null)
|
||||
{
|
||||
// 在未冻结状态下无法调用工厂方法,跳过
|
||||
}
|
||||
else if (descriptor.ImplementationType != null)
|
||||
{
|
||||
// 在未冻结状态下无法创建实例,跳过
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return CollectRegisteredImplementationInstances(typeof(T)).Cast<T>().ToList();
|
||||
}
|
||||
|
||||
var services = _provider!.GetServices<T>().ToList();
|
||||
@ -658,37 +625,17 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
/// </summary>
|
||||
/// <param name="type">服务类型</param>
|
||||
/// <returns>只读的服务实例列表</returns>
|
||||
/// <exception cref="InvalidOperationException">当容器未冻结时抛出</exception>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="type" /> 为 <see langword="null" /> 时抛出</exception>
|
||||
public IReadOnlyList<object> GetAll(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (_provider == null)
|
||||
{
|
||||
// 如果容器未冻结,从服务集合中获取已注册的实例
|
||||
var registeredServices = GetServicesUnsafe
|
||||
.Where(s => s.ServiceType == type || type.IsAssignableFrom(s.ServiceType))
|
||||
.ToList();
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var descriptor in registeredServices)
|
||||
{
|
||||
if (descriptor.ImplementationInstance != null)
|
||||
{
|
||||
result.Add(descriptor.ImplementationInstance);
|
||||
}
|
||||
else if (descriptor.ImplementationFactory != null)
|
||||
{
|
||||
// 在未冻结状态下无法调用工厂方法,跳过
|
||||
}
|
||||
else if (descriptor.ImplementationType != null)
|
||||
{
|
||||
// 在未冻结状态下无法创建实例,跳过
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return CollectRegisteredImplementationInstances(type);
|
||||
}
|
||||
|
||||
var services = _provider!.GetServices(type).ToList();
|
||||
@ -701,6 +648,108 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在容器未冻结时,从服务描述符中收集当前可直接观察到的实例绑定。
|
||||
/// </summary>
|
||||
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
|
||||
/// <returns>按当前未冻结语义可见的实例列表。</returns>
|
||||
/// <remarks>
|
||||
/// 该方法只读取 <see cref="ServiceDescriptor.ImplementationInstance" />,因为未冻结路径不会主动执行工厂方法,
|
||||
/// 也不会提前构造 <see cref="ServiceDescriptor.ImplementationType" />。
|
||||
/// 若同一实例同时经由多个可赋值的 <see cref="ServiceDescriptor.ServiceType" /> 暴露,
|
||||
/// 这里会把它视为兼容别名并只保留一个规范服务类型对应的结果;
|
||||
/// 但同一 <see cref="ServiceDescriptor.ServiceType" /> 的重复显式注册仍会完整保留,以维持注册顺序和多次注册语义。
|
||||
/// </remarks>
|
||||
private List<object> CollectRegisteredImplementationInstances(Type requestedServiceType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestedServiceType);
|
||||
|
||||
var matchingDescriptors = GetServicesUnsafe
|
||||
.Where(descriptor =>
|
||||
descriptor.ServiceType == requestedServiceType ||
|
||||
requestedServiceType.IsAssignableFrom(descriptor.ServiceType))
|
||||
.ToList();
|
||||
|
||||
if (matchingDescriptors.Count == 0)
|
||||
return [];
|
||||
|
||||
var preferredServiceTypes = BuildPreferredVisibleServiceTypes(matchingDescriptors, requestedServiceType);
|
||||
var result = new List<object>();
|
||||
foreach (var descriptor in matchingDescriptors)
|
||||
{
|
||||
if (descriptor.ImplementationInstance is { } instance)
|
||||
{
|
||||
if (preferredServiceTypes.TryGetValue(instance, out var preferredServiceType) &&
|
||||
preferredServiceType == descriptor.ServiceType)
|
||||
{
|
||||
result.Add(instance);
|
||||
}
|
||||
}
|
||||
else if (descriptor.ImplementationFactory != null)
|
||||
{
|
||||
// 在未冻结状态下无法调用工厂方法,跳过。
|
||||
}
|
||||
else if (descriptor.ImplementationType != null)
|
||||
{
|
||||
// 在未冻结状态下无法创建实例,跳过。
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为每个可见实例选择一个规范服务类型,避免同一实例因兼容别名重复出现在未冻结查询结果中。
|
||||
/// </summary>
|
||||
/// <param name="matchingDescriptors">已按请求类型过滤过的服务描述符集合。</param>
|
||||
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
|
||||
/// <returns>实例到其规范服务类型的映射。</returns>
|
||||
private static Dictionary<object, Type> BuildPreferredVisibleServiceTypes(
|
||||
IReadOnlyList<ServiceDescriptor> matchingDescriptors,
|
||||
Type requestedServiceType)
|
||||
{
|
||||
var preferredServiceTypes = new Dictionary<object, Type>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var instanceGroup in matchingDescriptors
|
||||
.Where(static descriptor => descriptor.ImplementationInstance is not null)
|
||||
.GroupBy(static descriptor => descriptor.ImplementationInstance!,
|
||||
ReferenceEqualityComparer.Instance))
|
||||
{
|
||||
preferredServiceTypes.Add(
|
||||
instanceGroup.Key,
|
||||
SelectPreferredVisibleServiceType(instanceGroup, requestedServiceType));
|
||||
}
|
||||
|
||||
return preferredServiceTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在“同一实例被多个服务类型暴露”的场景下,选择未冻结查询结果应保留的规范服务类型。
|
||||
/// </summary>
|
||||
/// <param name="descriptorsForInstance">引用同一实例的服务描述符。</param>
|
||||
/// <param name="requestedServiceType">调用方请求的服务类型。</param>
|
||||
/// <returns>应在结果中保留的服务类型。</returns>
|
||||
private static Type SelectPreferredVisibleServiceType(
|
||||
IEnumerable<ServiceDescriptor> descriptorsForInstance,
|
||||
Type requestedServiceType)
|
||||
{
|
||||
var serviceTypeGroups = descriptorsForInstance
|
||||
.GroupBy(static descriptor => descriptor.ServiceType)
|
||||
.Select((group, index) => new VisibleServiceTypeGroup(group.Key, group.Count(), index))
|
||||
.ToList();
|
||||
|
||||
// 若调用方请求的正是其中一个服务类型,优先保留它,使未冻结行为尽量贴近冻结后的精确服务解析口径。
|
||||
var requestedGroup = serviceTypeGroups.FirstOrDefault(group => group.ServiceType == requestedServiceType);
|
||||
if (requestedGroup is not null)
|
||||
return requestedGroup.ServiceType;
|
||||
|
||||
// 否则优先保留“同一服务类型下注册次数最多”的那组,避免显式多次注册被较宽泛的别名折叠掉。
|
||||
return serviceTypeGroups
|
||||
.OrderByDescending(static group => group.Count)
|
||||
.ThenBy(static group => group.FirstIndex)
|
||||
.First()
|
||||
.ServiceType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取并排序指定泛型类型的所有服务实例
|
||||
/// 主要用于系统调度场景
|
||||
@ -827,7 +876,6 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
|
||||
GetServicesUnsafe.Clear();
|
||||
_registeredInstances.Clear();
|
||||
_registeredCqrsHandlerAssemblyKeys.Clear();
|
||||
_provider = null;
|
||||
_logger.Info("Container cleared");
|
||||
}
|
||||
@ -899,16 +947,5 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CQRS handler 注册用的稳定程序集键。
|
||||
/// 该键需要同时兼顾真实程序集与测试中使用的 mocked Assembly,避免仅靠引用比较导致重复接入。
|
||||
/// </summary>
|
||||
/// <param name="assembly">目标程序集。</param>
|
||||
/// <returns>稳定的程序集标识字符串。</returns>
|
||||
private static string GetCqrsAssemblyRegistrationKey(Assembly assembly)
|
||||
{
|
||||
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
|
||||
|
||||
namespace GFramework.Core.Services.Modules;
|
||||
|
||||
@ -39,10 +39,15 @@ public sealed class CqrsRuntimeModule : IServiceModule
|
||||
|
||||
var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
|
||||
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
|
||||
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
|
||||
var runtime = CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger);
|
||||
var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
|
||||
|
||||
container.Register<ICqrsRuntime>(CqrsRuntimeFactory.CreateRuntime(container, dispatcherLogger));
|
||||
container.Register<ICqrsHandlerRegistrar>(
|
||||
CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger));
|
||||
container.Register(runtime);
|
||||
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime);
|
||||
container.Register<ICqrsHandlerRegistrar>(registrar);
|
||||
container.Register<ICqrsRegistrationService>(
|
||||
CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
13
GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs
Normal file
13
GFramework.Cqrs.Abstractions/Cqrs/ICqrsContext.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 定义 CQRS runtime 在分发期间携带的最小上下文标记。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该接口当前刻意保持为轻量 marker seam,只用于让 <see cref="ICqrsRuntime" /> 从
|
||||
/// <c>GFramework.Core.Abstractions</c> 的 <c>IArchitectureContext</c> 解耦。
|
||||
/// 运行时实现仍可在需要时识别更具体的上下文类型,并对现有 <c>IContextAware</c> 处理器执行兼容注入。
|
||||
/// </remarks>
|
||||
public interface ICqrsContext
|
||||
{
|
||||
}
|
||||
81
GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs
Normal file
81
GFramework.Cqrs.Abstractions/Cqrs/ICqrsRuntime.cs
Normal file
@ -0,0 +1,81 @@
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 定义架构上下文使用的 CQRS runtime seam。
|
||||
/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦,
|
||||
/// 使 CQRS runtime 契约可独立归属到 <c>GFramework.Cqrs.Abstractions</c>。
|
||||
/// </summary>
|
||||
public interface ICqrsRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// 发送请求并返回响应。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
/// <param name="context">当前 CQRS 分发上下文。</param>
|
||||
/// <param name="request">要分发的请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>请求响应。</returns>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// <paramref name="context" /> 或 <paramref name="request" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
/// <exception cref="System.InvalidOperationException">
|
||||
/// 当前上下文无法满足运行时要求,例如未找到对应请求处理器,或请求处理链中的
|
||||
/// <c>IContextAware</c> 对象需要 <c>IArchitectureContext</c> 但当前 <paramref name="context" /> 不提供该能力。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 该契约允许调用方传入任意 <see cref="ICqrsContext" />,
|
||||
/// 但默认运行时在需要向处理器或行为注入框架上下文时,仍要求该上下文同时实现 <c>IArchitectureContext</c>。
|
||||
/// </remarks>
|
||||
ValueTask<TResponse> SendAsync<TResponse>(
|
||||
ICqrsContext context,
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发布通知到所有已注册处理器。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNotification">通知类型。</typeparam>
|
||||
/// <param name="context">当前 CQRS 分发上下文。</param>
|
||||
/// <param name="notification">要发布的通知。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示通知分发完成的值任务。</returns>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// <paramref name="context" /> 或 <paramref name="notification" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
/// <exception cref="System.InvalidOperationException">
|
||||
/// 已解析到的通知处理器需要框架级上下文注入,但当前 <paramref name="context" /> 不提供
|
||||
/// <c>IArchitectureContext</c> 能力。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 默认实现允许零处理器场景静默完成;只有在处理器注入前置条件不满足时才会抛出异常。
|
||||
/// </remarks>
|
||||
ValueTask PublishAsync<TNotification>(
|
||||
ICqrsContext context,
|
||||
TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification;
|
||||
|
||||
/// <summary>
|
||||
/// 创建流式请求的异步响应序列。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流元素类型。</typeparam>
|
||||
/// <param name="context">当前 CQRS 分发上下文。</param>
|
||||
/// <param name="request">流式请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>按需生成的异步响应序列。</returns>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// <paramref name="context" /> 或 <paramref name="request" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
/// <exception cref="System.InvalidOperationException">
|
||||
/// 当前上下文无法满足运行时要求,例如未找到对应流式处理器,或流式处理链中的
|
||||
/// <c>IContextAware</c> 对象需要 <c>IArchitectureContext</c> 但当前 <paramref name="context" /> 不提供该能力。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 返回的异步序列在枚举前通常已完成处理器解析与上下文注入,
|
||||
/// 因此调用方应把 <paramref name="context" /> 视为整个枚举生命周期内的必需依赖。
|
||||
/// </remarks>
|
||||
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||||
ICqrsContext context,
|
||||
IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -13,6 +13,9 @@ namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
[TestFixture]
|
||||
internal sealed class CqrsHandlerRegistrarTests
|
||||
{
|
||||
private MicrosoftDiContainer? _container;
|
||||
private ArchitectureContext? _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化测试容器并重置共享状态。
|
||||
/// </summary>
|
||||
@ -42,9 +45,6 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
DeterministicNotificationHandlerState.Reset();
|
||||
}
|
||||
|
||||
private MicrosoftDiContainer? _container;
|
||||
private ArchitectureContext? _context;
|
||||
|
||||
/// <summary>
|
||||
/// 验证自动扫描到的通知处理器会按稳定名称顺序执行,而不是依赖反射枚举顺序。
|
||||
/// </summary>
|
||||
@ -188,6 +188,50 @@ internal sealed class CqrsHandlerRegistrarTests
|
||||
LoggerFactoryResolver.Provider = originalProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当生成注册器显式要求 reflection fallback 时,运行时会补扫剩余 handlers,
|
||||
/// 同时避免把已由生成注册器注册的映射重复写入服务集合。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Combine_Generated_Registry_With_Reflection_Fallback_Without_Duplicates()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns("GFramework.Core.Tests.Cqrs.PartialGeneratedRegistryAssembly, Version=1.0.0.0");
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
|
||||
.Returns([new CqrsReflectionFallbackAttribute()]);
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetTypes())
|
||||
.Returns(
|
||||
[
|
||||
typeof(GeneratedRegistryNotificationHandler),
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
|
||||
]);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
|
||||
var registrations = container.GetServicesUnsafe
|
||||
.Where(static descriptor =>
|
||||
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
|
||||
descriptor.ImplementationType is not null)
|
||||
.Select(static descriptor => descriptor.ImplementationType!)
|
||||
.ToList();
|
||||
|
||||
Assert.That(
|
||||
registrations,
|
||||
Is.EqualTo(
|
||||
[
|
||||
typeof(GeneratedRegistryNotificationHandler),
|
||||
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -337,3 +381,52 @@ internal sealed class GeneratedNotificationHandlerRegistry : ICqrsHandlerRegistr
|
||||
$"Registered CQRS handler {typeof(GeneratedRegistryNotificationHandler).FullName} as {typeof(INotificationHandler<GeneratedRegistryNotification>).FullName}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证“生成注册器 + reflection fallback”组合路径的私有嵌套处理器容器。
|
||||
/// </summary>
|
||||
internal sealed class ReflectionFallbackNotificationContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取仅能通过反射补扫接入的私有嵌套处理器类型。
|
||||
/// </summary>
|
||||
public static Type ReflectionOnlyHandlerType => typeof(ReflectionOnlyGeneratedRegistryNotificationHandler);
|
||||
|
||||
private sealed class ReflectionOnlyGeneratedRegistryNotificationHandler
|
||||
: 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>
|
||||
/// 模拟局部生成注册器场景中,仅注册“可由生成代码直接引用”的那部分 handlers。
|
||||
/// </summary>
|
||||
internal sealed class PartialGeneratedNotificationHandlerRegistry : 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}.");
|
||||
}
|
||||
}
|
||||
|
||||
14
GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
Normal file
14
GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 标记程序集中的 CQRS 生成注册器仍需要运行时补充反射扫描。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该特性通常由源码生成器自动添加到消费端程序集。
|
||||
/// 当生成器只能安全生成部分 handler 映射时,运行时会先执行生成注册器,再补一次带去重的反射扫描,
|
||||
/// 以覆盖那些生成代码无法直接引用的 handler 类型。
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
@ -48,4 +47,21 @@ public static class CqrsRuntimeFactory
|
||||
|
||||
return new DefaultCqrsHandlerRegistrar(container, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认的 CQRS 程序集注册协调器。
|
||||
/// </summary>
|
||||
/// <param name="registrar">底层 handler 注册器。</param>
|
||||
/// <param name="logger">用于注册阶段诊断的日志器。</param>
|
||||
/// <returns>默认 CQRS 程序集注册协调器。</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="registrar" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
|
||||
/// </exception>
|
||||
public static ICqrsRegistrationService CreateRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registrar);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
return new DefaultCqrsRegistrationService(registrar, logger);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,5 +3,6 @@ global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using System.Reflection;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using System.Diagnostics;
|
||||
|
||||
19
GFramework.Cqrs/ICqrsRegistrationService.cs
Normal file
19
GFramework.Cqrs/ICqrsRegistrationService.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 协调 CQRS 处理器程序集的接入流程。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该服务封装“程序集去重 + 生成注册器优先 + 反射回退”的默认接入语义,
|
||||
/// 让 <c>GFramework.Core</c> 的容器层只保留公开入口,而不再直接维护 CQRS handler 注册细节。
|
||||
/// </remarks>
|
||||
public interface ICqrsRegistrationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册一个或多个程序集中的 CQRS 处理器。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
void RegisterHandlers(IEnumerable<Assembly> assemblies);
|
||||
}
|
||||
@ -1,17 +1,17 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using ICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
|
||||
|
||||
namespace GFramework.Cqrs.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// GFramework 自有 CQRS 运行时分发器。
|
||||
/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前架构上下文。
|
||||
/// 该类型负责解析请求/通知处理器,并在调用前为上下文感知对象注入当前 CQRS 分发上下文。
|
||||
/// </summary>
|
||||
internal sealed class CqrsDispatcher(
|
||||
IIocContainer container,
|
||||
@ -38,11 +38,11 @@ internal sealed class CqrsDispatcher(
|
||||
/// 发布通知到所有已注册处理器。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNotification">通知类型。</typeparam>
|
||||
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="context">当前 CQRS 分发上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="notification">通知对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
public async ValueTask PublishAsync<TNotification>(
|
||||
IArchitectureContext context,
|
||||
ICqrsContext context,
|
||||
TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification : INotification
|
||||
@ -75,12 +75,12 @@ internal sealed class CqrsDispatcher(
|
||||
/// 发送请求并返回结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="context">当前 CQRS 分发上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="request">请求对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>请求响应。</returns>
|
||||
public async ValueTask<TResponse> SendAsync<TResponse>(
|
||||
IArchitectureContext context,
|
||||
ICqrsContext context,
|
||||
IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
@ -122,12 +122,12 @@ internal sealed class CqrsDispatcher(
|
||||
/// 创建流式请求并返回异步响应序列。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">响应元素类型。</typeparam>
|
||||
/// <param name="context">当前架构上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="context">当前 CQRS 分发上下文,用于上下文感知处理器注入。</param>
|
||||
/// <param name="request">流式请求对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>异步响应序列。</returns>
|
||||
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||||
IArchitectureContext context,
|
||||
ICqrsContext context,
|
||||
IStreamRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
@ -150,14 +150,20 @@ internal sealed class CqrsDispatcher(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为上下文感知处理器注入当前架构上下文。
|
||||
/// 为上下文感知处理器注入当前 CQRS 分发上下文。
|
||||
/// </summary>
|
||||
/// <param name="handler">处理器实例。</param>
|
||||
/// <param name="context">当前架构上下文。</param>
|
||||
private static void PrepareHandler(object handler, IArchitectureContext context)
|
||||
/// <param name="context">当前 CQRS 分发上下文。</param>
|
||||
private static void PrepareHandler(object handler, ICqrsContext context)
|
||||
{
|
||||
if (handler is IContextAware contextAware)
|
||||
contextAware.SetContext(context);
|
||||
{
|
||||
if (context is not IArchitectureContext architectureContext)
|
||||
throw new InvalidOperationException(
|
||||
"The current CQRS context does not implement IArchitectureContext, so it cannot be injected into IContextAware handlers.");
|
||||
|
||||
contextAware.SetContext(architectureContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
@ -32,7 +31,9 @@ internal static class CqrsHandlerRegistrar
|
||||
.Distinct()
|
||||
.OrderBy(GetAssemblySortKey, StringComparer.Ordinal))
|
||||
{
|
||||
if (TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger))
|
||||
var generatedRegistrationResult =
|
||||
TryRegisterGeneratedHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||
if (generatedRegistrationResult == GeneratedRegistrationResult.FullyHandled)
|
||||
continue;
|
||||
|
||||
RegisterAssemblyHandlers(container.GetServicesUnsafe, assembly, logger);
|
||||
@ -45,8 +46,11 @@ internal static class CqrsHandlerRegistrar
|
||||
/// <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)
|
||||
/// <returns>生成注册器的使用结果。</returns>
|
||||
private static GeneratedRegistrationResult TryRegisterGeneratedHandlers(
|
||||
IServiceCollection services,
|
||||
Assembly assembly,
|
||||
ILogger logger)
|
||||
{
|
||||
var assemblyName = GetAssemblySortKey(assembly);
|
||||
|
||||
@ -62,7 +66,7 @@ internal static class CqrsHandlerRegistrar
|
||||
.ToList();
|
||||
|
||||
if (registryTypes.Count == 0)
|
||||
return false;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
|
||||
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
||||
foreach (var registryType in registryTypes)
|
||||
@ -71,21 +75,21 @@ internal static class CqrsHandlerRegistrar
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
|
||||
return false;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
}
|
||||
|
||||
if (registryType.IsAbstract)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
|
||||
return false;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
}
|
||||
|
||||
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;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
}
|
||||
|
||||
registries.Add(registry);
|
||||
@ -98,7 +102,14 @@ internal static class CqrsHandlerRegistrar
|
||||
registry.Register(services, logger);
|
||||
}
|
||||
|
||||
return true;
|
||||
if (RequiresReflectionFallback(assembly))
|
||||
{
|
||||
logger.Debug(
|
||||
$"Generated CQRS registry for assembly {assemblyName} requested reflection fallback for unsupported handlers.");
|
||||
return GeneratedRegistrationResult.RequiresReflectionFallback;
|
||||
}
|
||||
|
||||
return GeneratedRegistrationResult.FullyHandled;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@ -106,7 +117,7 @@ internal static class CqrsHandlerRegistrar
|
||||
$"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;
|
||||
return GeneratedRegistrationResult.NoGeneratedRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +139,13 @@ internal static class CqrsHandlerRegistrar
|
||||
|
||||
foreach (var handlerInterface in handlerInterfaces)
|
||||
{
|
||||
if (IsHandlerMappingAlreadyRegistered(services, handlerInterface, implementationType))
|
||||
{
|
||||
logger.Debug(
|
||||
$"Skipping duplicate CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Request/notification handlers receive context injection before every dispatch.
|
||||
// Transient registration avoids sharing mutable Context across concurrent requests.
|
||||
services.AddTransient(handlerInterface, implementationType);
|
||||
@ -202,6 +220,29 @@ internal static class CqrsHandlerRegistrar
|
||||
definition == typeof(IStreamRequestHandler<,>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断生成注册器是否要求运行时继续补充反射扫描。
|
||||
/// </summary>
|
||||
private static bool RequiresReflectionFallback(Assembly assembly)
|
||||
{
|
||||
return assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), inherit: false)?.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断同一 handler 映射是否已经由生成注册器或先前扫描步骤写入服务集合。
|
||||
/// </summary>
|
||||
private static bool IsHandlerMappingAlreadyRegistered(
|
||||
IServiceCollection services,
|
||||
Type handlerInterface,
|
||||
Type implementationType)
|
||||
{
|
||||
// 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。
|
||||
// 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。
|
||||
return services.Any(descriptor =>
|
||||
descriptor.ServiceType == handlerInterface &&
|
||||
descriptor.ImplementationType == implementationType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成程序集排序键,保证跨运行环境的处理器注册顺序稳定。
|
||||
/// </summary>
|
||||
@ -217,4 +258,11 @@ internal static class CqrsHandlerRegistrar
|
||||
{
|
||||
return type.FullName ?? type.Name;
|
||||
}
|
||||
|
||||
private enum GeneratedRegistrationResult
|
||||
{
|
||||
NoGeneratedRegistry,
|
||||
FullyHandled,
|
||||
RequiresReflectionFallback
|
||||
}
|
||||
}
|
||||
|
||||
62
GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs
Normal file
62
GFramework.Cqrs/Internal/DefaultCqrsRegistrationService.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// 默认的 CQRS 程序集注册协调器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该实现把“按稳定程序集键去重”和“委托给 handler registrar 执行实际映射注册”收敛到 CQRS runtime 内部,
|
||||
/// 避免外层容器继续了解 handler 注册流水线的内部结构。
|
||||
/// <para>
|
||||
/// 该类型不是线程安全的。调用方应在外部同步边界内访问 <see cref="RegisterHandlers" />,
|
||||
/// 例如由容器写锁串行化程序集注册流程。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class DefaultCqrsRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger)
|
||||
: ICqrsRegistrationService
|
||||
{
|
||||
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
private readonly HashSet<string> _registeredAssemblyKeys = new(StringComparer.Ordinal);
|
||||
private readonly ICqrsHandlerRegistrar _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
|
||||
|
||||
/// <summary>
|
||||
/// 注册指定程序集中的 CQRS handlers。
|
||||
/// </summary>
|
||||
/// <param name="assemblies">要接入的程序集集合。</param>
|
||||
public void RegisterHandlers(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
|
||||
var processedAssemblyKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var assembly in assemblies
|
||||
.Where(static assembly => assembly is not null)
|
||||
.OrderBy(GetAssemblyRegistrationKey, StringComparer.Ordinal))
|
||||
{
|
||||
var assemblyKey = GetAssemblyRegistrationKey(assembly);
|
||||
if (!processedAssemblyKeys.Add(assemblyKey))
|
||||
continue;
|
||||
|
||||
if (_registeredAssemblyKeys.Contains(assemblyKey))
|
||||
{
|
||||
_logger.Debug(
|
||||
$"Skipping CQRS handler registration for assembly {assemblyKey} because it was already registered.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_registrar.RegisterHandlers([assembly]);
|
||||
_registeredAssemblyKeys.Add(assemblyKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成稳定程序集键,避免相同程序集被不同 <see cref="Assembly" /> 实例重复接入。
|
||||
/// </summary>
|
||||
/// <param name="assembly">目标程序集。</param>
|
||||
/// <returns>稳定的程序集标识。</returns>
|
||||
private static string GetAssemblyRegistrationKey(Assembly assembly)
|
||||
{
|
||||
return assembly.FullName ?? assembly.GetName().Name ?? assembly.ToString();
|
||||
}
|
||||
}
|
||||
@ -61,6 +61,11 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
@ -120,10 +125,120 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会放弃产出并让运行时回退到反射扫描。
|
||||
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器仍会为可见 handlers 生成注册器,
|
||||
/// 并额外标记运行时补充反射扫描。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Skips_Generation_When_Assembly_Contains_Private_Nested_Handler()
|
||||
public async Task
|
||||
Generates_Visible_Handlers_And_Requests_Reflection_Fallback_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.Cqrs.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> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
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) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.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> { }
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]
|
||||
[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute()]
|
||||
|
||||
namespace GFramework.Generated.Cqrs;
|
||||
|
||||
internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.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.Cqrs.Abstractions.Cqrs.IRequestHandler<global::TestApp.VisibleRequest, string>),
|
||||
typeof(global::TestApp.VisibleHandler));
|
||||
logger.Debug("Registered CQRS handler TestApp.VisibleHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<TestApp.VisibleRequest, string>.");
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
|
||||
source,
|
||||
("CqrsHandlerRegistry.g.cs", expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当旧版 runtime 合同中不存在 reflection fallback 标记特性时,
|
||||
/// 生成器会保留此前的整程序集回退行为,避免丢失不可见 handlers。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Skips_Generation_For_Unsupported_Handler_When_Fallback_Marker_Is_Unavailable()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
@ -16,6 +16,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
|
||||
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
|
||||
|
||||
private const string CqrsReflectionFallbackAttributeMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute";
|
||||
|
||||
private const string CqrsHandlerRegistryAttributeMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
||||
|
||||
@ -28,8 +31,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
/// <inheritdoc />
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var generationEnabled = context.CompilationProvider
|
||||
.Select(static (compilation, _) => HasRequiredTypes(compilation));
|
||||
var generationEnvironment = context.CompilationProvider
|
||||
.Select(static (compilation, _) => CreateGenerationEnvironment(compilation));
|
||||
|
||||
// Restrict semantic analysis to type declarations that can actually contribute implemented interfaces.
|
||||
var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
@ -39,19 +42,24 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(
|
||||
generationEnabled.Combine(handlerCandidates),
|
||||
generationEnvironment.Combine(handlerCandidates),
|
||||
static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right));
|
||||
}
|
||||
|
||||
private static bool HasRequiredTypes(Compilation compilation)
|
||||
private static GenerationEnvironment CreateGenerationEnvironment(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;
|
||||
var generationEnabled = 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;
|
||||
|
||||
return new GenerationEnvironment(
|
||||
generationEnabled,
|
||||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null);
|
||||
}
|
||||
|
||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||
@ -108,21 +116,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
false);
|
||||
}
|
||||
|
||||
private static void Execute(SourceProductionContext context, bool generationEnabled,
|
||||
private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment,
|
||||
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
||||
{
|
||||
if (!generationEnabled)
|
||||
if (!generationEnvironment.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)
|
||||
if (registrations.Count == 0)
|
||||
return;
|
||||
|
||||
context.AddSource(HintName, GenerateSource(registrations));
|
||||
// If the runtime contract does not yet expose the reflection fallback marker,
|
||||
// keep the previous all-or-nothing behavior so unsupported handlers are not silently dropped.
|
||||
if (hasUnsupportedConcreteHandler && !generationEnvironment.SupportsReflectionFallbackMarker)
|
||||
return;
|
||||
|
||||
context.AddSource(
|
||||
HintName,
|
||||
GenerateSource(registrations, hasUnsupportedConcreteHandler));
|
||||
}
|
||||
|
||||
private static List<HandlerRegistrationSpec> CollectRegistrations(
|
||||
@ -144,7 +156,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
if (candidate.Value.HasUnsupportedConcreteHandler)
|
||||
{
|
||||
hasUnsupportedConcreteHandler = true;
|
||||
return [];
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
|
||||
@ -270,7 +282,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return GetTypeSortKey(type).Replace("global::", string.Empty);
|
||||
}
|
||||
|
||||
private static string GenerateSource(IReadOnlyList<HandlerRegistrationSpec> registrations)
|
||||
private static string GenerateSource(
|
||||
IReadOnlyList<HandlerRegistrationSpec> registrations,
|
||||
bool emitReflectionFallbackAttribute)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
@ -283,6 +297,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
builder.Append('.');
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.AppendLine("))]");
|
||||
if (emitReflectionFallbackAttribute)
|
||||
{
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".CqrsReflectionFallbackAttribute()]");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
builder.Append("namespace ");
|
||||
builder.Append(GeneratedNamespace);
|
||||
@ -399,4 +420,8 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct GenerationEnvironment(
|
||||
bool GenerationEnabled,
|
||||
bool SupportsReflectionFallbackMarker);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Cqrs;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Cqrs;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using GFramework.Cqrs.Command;
|
||||
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
|
||||
|
||||
namespace GFramework.Tests.Common;
|
||||
|
||||
@ -60,7 +60,12 @@ public static class CqrsTestRuntime
|
||||
{
|
||||
var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
|
||||
var runtime = CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger);
|
||||
container.Register<ICqrsRuntime>(runtime);
|
||||
container.Register(runtime);
|
||||
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)runtime);
|
||||
}
|
||||
else if (container.Get<LegacyICqrsRuntime>() is null)
|
||||
{
|
||||
container.Register<LegacyICqrsRuntime>((LegacyICqrsRuntime)container.GetRequired<ICqrsRuntime>());
|
||||
}
|
||||
|
||||
if (container.Get<ICqrsHandlerRegistrar>() is null)
|
||||
@ -69,6 +74,14 @@ public static class CqrsTestRuntime
|
||||
var registrar = CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
|
||||
container.Register<ICqrsHandlerRegistrar>(registrar);
|
||||
}
|
||||
|
||||
if (container.Get<ICqrsRegistrationService>() is null)
|
||||
{
|
||||
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
|
||||
var registrar = container.GetRequired<ICqrsHandlerRegistrar>();
|
||||
var registrationService = CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger);
|
||||
container.Register<ICqrsRegistrationService>(registrationService);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user