.NET 依赖注入容器兼容性测试套件解析:用 Microsoft.Extensions.DependencyInjection.Specification.Tests 验证自定义 DI 容器

.NET 依赖注入容器兼容性测试套件解析:用 Microsoft.Extensions.DependencyInjection.Specification.Tests 验证自定义 DI 容器 .NET 依赖注入容器兼容性测试套件解析用 Microsoft.Extensions.DependencyInjection.Specification.Tests 验证自定义 DI 容器【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读Microsoft.Extensions.DependencyInjection.Specification.Tests是 dotnet/runtime 仓库中用于验证 DI 容器与Microsoft.Extensions.DependencyInjection框架行为一致性的官方规范测试套件。本文将以该包的 PACKAGE.md 为骨架结合其测试源码、Fakes 辅助类型与仓库内真实使用示例系统讲解这套规范测试的定位、接入方式、测试覆盖矩阵以及底层断言逻辑帮助自定义 DI 容器开发者快速完成合规性验证。一、包定位为什么需要一套规范测试Microsoft.Extensions.DependencyInjection是 .NET 生态中最基础的依赖注入抽象层。它的价值不仅仅在于自带一个ServiceProvider实现更在于定义了一套行为规范——任何第三方容器只要遵循这套规范就能无缝接入 ASP.NET Core、Worker Service 等基于 DI 抽象构建的应用。Microsoft.Extensions.DependencyInjection.Specification.Tests正是用来检验行为一致性的工具它提供了一组基于 xUnit.net 的测试基类专门针对自行实现 DI 容器的开发者设计。通过继承测试基类并接入自己的容器开发者可以验证服务的解析行为Transient / Scoped / Singleton 生命周期语义构造器注入、工厂注入、实例注入等注册方式泛型开放服务、带约束泛型服务的解析Scope 的创建、嵌套与释放语义可释放IDisposable服务的生命周期管理Keyed Service键控服务的完整语义。该包在仓库中的物理位置为 src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/其项目文件Microsoft.Extensions.DependencyInjection.Specification.Tests.csproj将自身描述为Suite of xUnit.net tests to check for container compatibility with Microsoft.Extensions.DependencyInjection并分别引用xunit.assert与xunit.extensibility.core两个 NuGet 包作为断言基础设施。二、快速上手让自定义容器通过规范测试2.1 项目准备按照 PACKAGE.md 的指引创建一个新的测试项目并添加如下 NuGet 依赖Microsoft.Extensions.DependencyInjection.Specification.Tests提供测试基类与 Fakes 辅助类型xunit测试运行框架自定义 DI 容器包例如MyCustomDI。2.2 编写测试类PACKAGE.md 给出了最小接入示例新建一个测试类继承DependencyInjectionSpecificationTests并重写CreateServiceProvider方法让它返回自定义容器的IServiceProviderusing Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Specification.Tests; using Xunit; using MyCustomDI; public class MyCustomDIContainerTests : DependencyInjectionSpecificationTests { protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) { // Create an instance of your custom DI container and build the service provider return MyCustomDIContainer.BuildServiceProvider(serviceCollection); } }IServiceCollection本质上是ServiceDescriptor的列表。测试基类内部使用一个名为TestServiceCollection的最小实现见 ServiceCollection.cs来构建测试注册因此不依赖任何具体容器——这正是规范测试可以横跨不同容器运行的关键。2.3 运行与验证执行测试即可看到大量规范用例在你的容器上运行。如果全部通过说明容器在测试覆盖的语义维度上与Microsoft.Extensions.DependencyInjection保持一致。2.4 接入点详解CreateServiceProvider 签名基类中定义的抽象方法签名见 DependencyInjectionSpecificationTests.cs为protected abstract IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection);实现方需要把传入的serviceCollection中的全部ServiceDescriptor注册到自己的容器中并返回一个实现了IServiceProvider以及必要的IDisposable、IServiceScopeFactory等约定的提供者实例。三、主类型一览PACKAGE.md 明确列出该包的两个核心测试基类类型命名空间作用DependencyInjectionSpecificationTestsMicrosoft.Extensions.DependencyInjection.Specification非键控unkeyedDI 行为的规范测试KeyedDependencyInjectionSpecificationTestsMicrosoft.Extensions.DependencyInjection.Specification键控keyedDI 行为的规范测试两者都声明为public abstract partial class通过 partial 关键字将不同主题的测试拆分在多个文件中DependencyInjectionSpecificationTests.cs核心解析语义测试ActivatorUtilitiesTests.csActivatorUtilities激活器兼容性测试KeyedDependencyInjectionSpecificationTests.cs键控服务测试ServiceProviderIsServiceSpecificationTests.cs 与 ServiceProviderIsKeyedServiceSpecificationTests.csIServiceProviderIsService/IServiceProviderIsKeyedService能力测试。除此之外包内还提供一组名为Microsoft.Extensions.DependencyInjection.Specification.Fakes的模拟类型见 Fakes/如IFakeService、FakeService、IFakeOpenGenericServiceT、FakeDisposeCallback等覆盖常见注入场景。四、规范测试覆盖的行为矩阵理解规范测试的最好方式是逐一阅读它断言了哪些行为。以下按主题梳理 DependencyInjectionSpecificationTests.cs 中的关键测试。4.1 生命周期语义Transient 每次解析返回新实例ServicesRegisteredWithImplementationType_ReturnDifferentInstancesPerResolution_ForTransientServices断言两次GetServiceIFakeService()返回NotSame。Singleton 返回同一实例ServicesRegisteredWithImplementationType_ReturnSameInstancesPerResolution_ForSingletons断言两次解析Same。Scoped 作用域内共享ScopedServiceCanBeResolved断言同一 Scope 内解析两次返回相同实例而根 Provider 与 Scope 之间实例不同。嵌套 Scope 相互隔离NestedScopedServiceCanBeResolved在 outer/inner 两个 Scope 中解析 Scoped 服务并断言NotSame。4.2 注册方式类型注册ServicesRegisteredWithImplementationTypeCanBeResolved使用AddTransient(typeof(IFakeService), typeof(FakeService))并断言解析结果类型正确。实例注册ServiceInstanceCanBeResolved使用AddSingleton(typeof(IFakeServiceInstance), instance)并断言返回的就是那个预置实例。工厂注册FactoryServicesCanBeCreatedByGetService通过AddTransientIFactoryService(p ...)注册工厂并验证工厂内通过p.GetRequiredServiceIFakeService()进行的依赖解析以及工厂返回对象的属性如Value 42是否正确。多注册与覆盖LastServiceReplacesPreviousServices验证多次注册同一服务类型时最后一次注册生效。4.3 集合解析与注册顺序SingleServiceCanBeIEnumerableResolved与MultipleServiceCanBeIEnumerableResolved验证IEnumerableTService的解析RegistrationOrderIsPreservedWhenServicesAreIEnumerableResolved是关键顺序测试它先按FakeOneMultipleService、FakeTwoMultipleService顺序注册解析断言顺序一致随后collection.Reverse()再次构建 Provider断言结果顺序也随之反转——IEnumerable 解析必须保留注册顺序。4.4 泛型开放服务与约束IFakeOpenGenericServiceT与FakeOpenGenericServiceT是测试泛型语义的核心类型OpenGenericServicesCanBeResolvedAddTransient(typeof(IFakeOpenGenericService), typeof(FakeOpenGenericService))后解析IFakeOpenGenericServiceIFakeSingletonServiceConstrainedOpenGenericServicesCanBeResolved同时注册无约束与带约束ConstrainedFakeOpenGenericServiceT的实现验证容器能按类型参数约束正确分派ClosedServicesPreferredOverOpenGenericServices同时存在闭合泛型注册与开放泛型注册时闭合注册优先InterfaceConstrainedOpenGenericServicesCanBeResolved、AbstractClassConstrainedOpenGenericServicesCanBeResolved验证接口约束、抽象类约束下的解析行为ResolvesMixedOpenClosedGenericsAsEnumerable混合开放/闭合泛型注册按IEnumerable解析时数量与顺序正确。Fakes 目录中准备了大量用于测试约束语义的辅助类型如ClassWithInterfaceConstraintT、ClassWithAbstractClassConstraintT、ClassWithClassConstraint、ClassWithStructConstraint等覆盖where T : class、where T : struct、where T : new()、自引用约束等各类泛型约束。4.5 构造器选择ServiceContainerPicksConstructorWithLongestMatchesTheory通过TypeWithSupersetConstructors的多构造器场景验证容器应选择可满足参数最多的构造器。测试数据ServiceContainerPicksConstructorWithLongestMatchesData枚举了从 1 到 4 个可注入服务逐级叠加的场景。ClassWithAmbiguousCtors、ClassWithAmbiguousCtorsAndAttribute等 Fakes 则用于覆盖模棱两可构造器及ActivatorUtilitiesConstructorAttribute标记的场景。4.6 Scope、释放与 Dispose 语义DI 的释放契约是容器兼容性中最容易被忽略、也最容易出错的环节规范测试对此有严格断言DisposingScopeDisposesServiceScope 释放时其中创建的 Scoped 与 Transient 可释放服务应被释放而 Singleton不应被 Scope 释放直到整个 Provider 释放时才释放 Singleton。DisposesInReverseOrderOfCreation释放顺序必须是创建顺序的逆序——外层服务先于其依赖的内层服务被释放。SingletonServicesComeFromRootProviderSingleton 实例由根 Provider 持有跨 Scope 共享同一实例且不随任何 Scope 的释放而释放。ScopesAreFlatNotHierarchicalScope 是扁平的而非层级式的——即使外层 Scope 已释放内层 Scope 仍能正常工作且从内层仍可解析 Singleton。ScopedServices_FromCachedScopeFactory_CanBeResolvedAndDisposed通过缓存的IServiceScopeFactory反复创建嵌套 Scope验证释放边界。ServiceProviderIsDisposableProvider 本身必须实现IDisposable。SafelyDisposeNestedProviderReferences通过ClassWithNestedReferencesToProvider验证持有嵌套 Provider 引用的对象可安全释放。4.7 不存在服务的解析AttemptingToResolveNonexistentServiceReturnsNullGetService返回nullNonexistentServiceCanBeIEnumerableResolvedIEnumerable解析返回空集合。4.8 IServiceProvider 自解析SelfResolveThenDispose验证GetServiceIServiceProvider()应能解析到自身通常解析到当前 Scope 的 Provider。五、键控服务Keyed Services规范测试KeyedDependencyInjectionSpecificationTests是 .NET 8 引入键控服务后新增的规范测试基类覆盖AddKeyedTransient/Scoped/Singleton、[FromKeyedServices]注入、KeyedService.AnyKey等全部键控语义。其核心源码位于 KeyedDependencyInjectionSpecificationTests.cs。5.1 键控服务的解析规则表CombinationalRegistration测试用一个精心构造的注册组合把键控语义的完整规则以表格形式固化在源码注释中这是理解键控 DI 最权威的行为说明查询方式KeyedUnkeyedAnyKeynull keyGetServices(Type)否是否是GetService(Type)否是否是GetKeyedServices(null)否是否是GetKeyedService(null)否是否是GetKeyedServices(AnyKey)是否否否GetKeyedService(AnyKey)抛出异常抛出异常抛出异常抛出异常GetKeyedServices(key)是否否否GetKeyedService(key)是否是否规则要点来自源码注释null key 等价于非键控GetKeyedServices(null)与GetServices()返回相同结果这使键控 API 能够同时支持键控与非键控注册AnyKey 是键控的特例KeyedService.AnyKey注册可匹配任意非 null keyAnyKey 注册不会出现在GetKeyedServices(AnyKey)结果中且GetKeyedService(AnyKey)总是抛出InvalidOperationExceptionIEnumerable 结果保持注册顺序Singleton 解析时最后一个匹配者胜出。5.2 键控生命周期ResolveKeyedServiceSingletonFactoryWithAnyKeyAnyKey 工厂注册的 Singleton按不同 key 查询返回同一实例ResolveKeyedServiceTransientFactory/ResolveKeyedServiceTransientTypeTransient 每次解析返回不同实例ResolveKeyedSingletonFromScopeServiceProvider/ResolveKeyedScopedFromScopeServiceProvider/ResolveKeyedTransientFromScopeServiceProvider验证跨 Scope 时 Singleton 相同、Scoped 不同、Transient 不同。5.3 键控依赖注入ResolveKeyedServiceSingletonInstanceWithKeyInjection通过AddKeyedSingletonIService, Service(serviceKey)注册服务内部可通过[FromKeyedServices]拿到注入的 keyResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithDefaults键控参数缺失但构造器参数有默认值时仍可解析ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithUnkeyedService非键控注册不能回退给键控参数注入——此时解析抛InvalidOperationException。六、ActivatorUtilities 兼容性测试ActivatorUtilitiesTests.cs 验证容器对ActivatorUtilities.CreateInstance/CreateFactory的支持。测试通过CreateInstanceFuncs同时覆盖两种调用路径private static object CreateInstanceDirectly(IServiceProvider provider, Type type, object[] args) ActivatorUtilities.CreateInstance(provider, type, args); private static object CreateInstanceFromFactory(IServiceProvider provider, Type type, object[] args) { var factory ActivatorUtilities.CreateFactory(type, args.Select(a a.GetType()).ToArray()); return factory(provider, args); }覆盖的行为包括未注册到容器中的类型也能通过激活器创建前提是其构造器依赖可从 Provider 解析支持向构造器传递额外参数TypeActivatorAcceptsAnyNumberOfAdditionalConstructorParametersToProvide支持静态构造器、可选参数构造器、带结构体默认值的可选参数构造器通过ExpectStructWithPublicDefaultConstructorInvoked虚属性暴露可覆盖的钩子——因为不同容器在结构体默认构造器是否被调用上存在实现差异规范测试允许容器作者按需调整该行为期望。七、仓库内的真实应用容器作者如何跳过不支持的用例规范测试设计上允许容器作者对个别无法满足的用例做豁免仓库内的真实示例位于 src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/其中为 Autofac、DryIoc、Grace、Lamar、LightInject、StashBox 等第三方容器各建了适配测试项目。以 Autofac 为例Autofac.cspublic class AutofacDependencyInjectionSpecificationTests : SkippableDependencyInjectionSpecificationTests { public override string[] SkippedTests new[] { ScopesAreFlatNotHierarchical, ServiceScopeFactoryIsSingleton }; protected override IServiceProvider CreateServiceProviderImpl(IServiceCollection serviceCollection) { var builder new ContainerBuilder(); builder.Populate(serviceCollection); return new AutofacServiceProvider(builder.Build()); } }其中SkippableDependencyInjectionSpecificationTests见 SkippableDependencyInjectionSpecificationTests.cs实现了按测试方法名跳过的机制当发现当前执行的测试方法名位于SkippedTests数组中时就返回默认的serviceCollection.BuildServiceProvider()即微软官方容器让该用例必然通过从而在不破坏其他用例的前提下实现对个别语义差异的显式豁免。仓库自身的ServiceProvider实现则直接继承测试基类、不跳过任何用例见 DI.Tests/ServiceProviderContainerTests.cs并通过 KeyedServiceProviderContainerTests.cs 分别验证默认容器、动态容器、表达式容器与 IL Emit 容器四种实现路径。八、Fakes 辅助类型速查包内的 Fakes/ 目录提供了一整套可复用的模拟类型接入测试时无需自行编写。常用类型与用途如下类型用途IFakeService/FakeService最基础的服务接口与实现用于生命周期与解析测试IFakeScopedService/IFakeSingletonService/IFakeServiceInstance分别对应 Scoped、Singleton、实例注册场景IFakeOpenGenericServiceT/FakeOpenGenericServiceT开放泛型服务解析测试IFakeMultipleService/FakeOneMultipleService/FakeTwoMultipleService多注册与集合解析测试FakeDisposeCallback/FakeDisposableCallbackOuterService等验证释放顺序与 Dispose 回调ClassWithServiceProvider/ClassWithNestedReferencesToProvider验证 Provider 注入与嵌套引用释放TypeWithSupersetConstructors多构造器选择测试ClassWithPrivateCtor/ClassWithInternalConstructor/ClassWithStaticCtor/ClassWithThrowingCtor边界构造器场景这些类型与测试方法一一对应容器作者可借助它们快速定位失败用例的具体语义。九、常见失败模式与排查建议结合规范测试的断言内容自定义容器最常见的兼容性缺陷集中在以下几类释放语义错误Scope 释放时误释放了 Singleton或释放顺序不是创建逆序——对应DisposingScopeDisposesService、DisposesInReverseOrderOfCreation失败构造器选择策略不符未按可满足参数最多原则选择构造器——对应ServiceContainerPicksConstructorWithLongestMatches失败IEnumerable 顺序不保集合解析时未保持注册顺序——对应RegistrationOrderIsPreservedWhenServicesAreIEnumerableResolved失败泛型约束分派错误带约束的开放泛型实现未被正确过滤——对应ConstrainedOpenGenericServicesCanBeResolved等用例失败键控语义缺失GetKeyedService(AnyKey)未抛出异常、null key 未与非键控等价等——对应CombinationalRegistration失败。排查时可先在测试项目上运行dotnet test --filter FullyQualifiedName~失败用例名定位单个用例再结合本文第四、五节的行为说明对照容器实现。十、小结Microsoft.Extensions.DependencyInjection.Specification.Tests是 .NET DI 生态中契约即测试理念的典型实践它把Microsoft.Extensions.DependencyInjection的行为规范固化为可执行的 xUnit 测试让任何第三方容器都能以最小成本获得与官方实现一致的语义保证。无论是接入 ASP.NET Core 生态的框架作者还是内部自研容器的团队继承DependencyInjectionSpecificationTests/KeyedDependencyInjectionSpecificationTests并跑通全量用例都是验证容器合规性的最直接途径。该包以 MIT 许可证开源bug 报告与贡献请提交至 dotnet/runtime 仓库其工程属性、Fakes 类型与全部测试源码均可在 src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/ 目录下继续深入研读。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考