mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-09 10:19:00 +08:00
feat(ioc): 添加Microsoft DI容器适配器及测试
- 实现MicrosoftDiContainer类作为IIocContainer接口的适配器 - 提供线程安全的依赖注入容器功能 - 支持单例、瞬态、作用域服务注册 - 实现CQRS处理器注册功能 - 添加服务工厂方法注册支持 - 实现按优先级排序的服务获取功能 - 添加完整的单元测试覆盖基本功能和边界情况 - 支持容器冻结和作用域创建功能 - 实现多样性实例注册到多个接口的功能
This commit is contained in:
parent
00a1038d0a
commit
0d9d09bc4a
@ -249,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>
|
||||
@ -716,6 +756,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>
|
||||
|
||||
@ -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>
|
||||
@ -593,32 +601,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>();
|
||||
var seenInstances = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var descriptor in registeredServices)
|
||||
{
|
||||
if (descriptor.ImplementationInstance is T instance)
|
||||
{
|
||||
// 同一实例可能同时以“正式接口 + 兼容别名接口”被注册;未冻结路径需去重以保持与冻结后的解析口径一致。
|
||||
if (seenInstances.Add(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();
|
||||
@ -636,40 +619,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>();
|
||||
var seenInstances = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var descriptor in registeredServices)
|
||||
{
|
||||
if (descriptor.ImplementationInstance != null)
|
||||
{
|
||||
// 同一实例可能通过多个可赋值服务类型暴露;返回前按引用去重,避免兼容别名造成重复观察结果。
|
||||
if (seenInstances.Add(descriptor.ImplementationInstance))
|
||||
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();
|
||||
@ -682,6 +642,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>
|
||||
/// 获取并排序指定泛型类型的所有服务实例
|
||||
/// 主要用于系统调度场景
|
||||
|
||||
@ -15,6 +15,17 @@ public interface ICqrsRuntime
|
||||
/// <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,
|
||||
@ -28,6 +39,16 @@ public interface ICqrsRuntime
|
||||
/// <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,
|
||||
@ -42,6 +63,17 @@ public interface ICqrsRuntime
|
||||
/// <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,
|
||||
|
||||
@ -236,6 +236,8 @@ internal static class CqrsHandlerRegistrar
|
||||
Type handlerInterface,
|
||||
Type implementationType)
|
||||
{
|
||||
// 这里保持线性扫描,避免为常见的小到中等规模程序集长期维护额外索引。
|
||||
// 若未来大型服务集合出现热点,可在更高层批处理中引入 HashSet<(Type, Type)> 做 O(1) 去重。
|
||||
return services.Any(descriptor =>
|
||||
descriptor.ServiceType == handlerInterface &&
|
||||
descriptor.ImplementationType == implementationType);
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
@ -10,6 +9,10 @@ namespace GFramework.Cqrs.Internal;
|
||||
/// <remarks>
|
||||
/// 该实现把“按稳定程序集键去重”和“委托给 handler registrar 执行实际映射注册”收敛到 CQRS runtime 内部,
|
||||
/// 避免外层容器继续了解 handler 注册流水线的内部结构。
|
||||
/// <para>
|
||||
/// 该类型不是线程安全的。调用方应在外部同步边界内访问 <see cref="RegisterHandlers" />,
|
||||
/// 例如由容器写锁串行化程序集注册流程。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class DefaultCqrsRegistrationService(ICqrsHandlerRegistrar registrar, ILogger logger)
|
||||
: ICqrsRegistrationService
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user