feat(ioc): 添加Microsoft DI容器适配器及测试

- 实现MicrosoftDiContainer类作为IIocContainer接口的适配器
- 提供线程安全的依赖注入容器功能
- 支持单例、瞬态、作用域服务注册
- 实现CQRS处理器注册功能
- 添加服务工厂方法注册支持
- 实现按优先级排序的服务获取功能
- 添加完整的单元测试覆盖基本功能和边界情况
- 支持容器冻结和作用域创建功能
- 实现多样性实例注册到多个接口的功能
This commit is contained in:
GeWuYou 2026-04-16 09:14:27 +08:00
parent 00a1038d0a
commit 0d9d09bc4a
5 changed files with 215 additions and 54 deletions

View File

@ -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>

View File

@ -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>
/// 获取并排序指定泛型类型的所有服务实例
/// 主要用于系统调度场景

View File

@ -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,

View File

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

View File

@ -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