.NET 依赖注入容器 Microsoft.Extensions.DependencyInjection 完全指南:从 IoC 入门到 ServiceProvider 源码解析
2026/9/20 13:25:05 网站建设 项目流程
  • 语言运行时
  • 标准库
  • JIT编译
  • 编译器

【免费下载链接】runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

项目地址:https://gitcode.com/GitHub_Trending/runtime6/runtime
点击查看免费下载

Microsoft.Extensions.DependencyInjection是 .NET 官方提供的高性能依赖注入(DI)容器实现,位于本仓库 src/libraries/Microsoft.Extensions.DependencyInjection 目录下,它实现了Microsoft.Extensions.DependencyInjection.Abstractions包中定义的全部 DI 接口,是 ASP.NET Core、.NET Generic Host 以及绝大多数现代 .NET 应用实现控制反转(IoC)的基石。读完本文,你将掌握服务的注册、解析、生命周期管理、作用域(Scope)与校验配置等全套实战技能,并能从源码层面理解ServiceProvider的编译式解析引擎与内建服务的工作原理。

一、包定位:它解决什么问题

依赖注入(Dependency Injection,DI)是一种软件设计模式,其核心目的是实现类与其依赖之间的控制反转(Inversion of Control,IoC):一个类不再自行new出它需要的依赖,而是通过构造函数、属性或方法由外部容器注入,从而降低耦合、提升可测试性与可维护性。

本包(Microsoft.Extensions.DependencyInjection)正是这一模式的默认容器实现。正如包的 PACKAGE.md 所述,它的关键能力是:提供Microsoft.Extensions.DependencyInjection.Abstractions包中 DI 接口的具体实现。也就是说,你平时接触的IServiceCollectionIServiceProviderIServiceScopeFactory等接口定义在 Abstractions 包中,而真正干活、负责创建和缓存实例的容器本体是这个包。

二、五分钟上手:从零构建一个 DI 容器

包的 PACKAGE.md 给出了最经典的入门示例,这里完整保留并展开说明:

ServiceCollection services = new (); services.AddSingleton<IMessageWriter, MessageWriter>(); using ServiceProvider provider = services.BuildServiceProvider(); // The code below, following the IoC pattern, is typically only aware of the IMessageWriter interface, not the implementation. IMessageWriter messageWriter = provider.GetService<IMessageWriter>()!; messageWriter.Write("Hello"); public interface IMessageWriter { void Write(string message); } internal class MessageWriter : IMessageWriter { public void Write(string message) { Console.WriteLine($"MessageWriter.Write(message: \"{message}\")"); } }

这段代码演示了 DI 的完整三步骤,也是整个模式的精髓:

  1. 创建注册集合new ServiceCollection()创建一个服务描述符(ServiceDescriptor)集合,ServiceCollection类的实现在 ServiceCollection.cs。
  2. 注册服务services.AddSingleton<IMessageWriter, MessageWriter>()将抽象接口IMessageWriter与具体实现MessageWriter建立映射,并声明其生命周期为Singleton。这个扩展方法定义于 Abstractions 包的 ServiceCollectionServiceExtensions.cs,除AddSingleton外还有AddScopedAddTransient等重载。
  3. 构建并解析BuildServiceProvider()将集合编译为ServiceProvider;业务代码只依赖IMessageWriter接口,对MessageWriter的具体实现完全无感——这正是 IoC 的核心价值:面向抽象编程,实现可随时替换(例如换成 Mock 或日志代理)。

2.1 关于!(null 容忍)的说明

示例中provider.GetService<IMessageWriter>()!!是 null-forgiving 运算符,因为GetService的签名允许返回null(服务未注册时)。如果你确定服务一定存在,更推荐使用GetRequiredService<T>()——它会在服务缺失时抛出InvalidOperationException("No service registered for type..."),详见 ServiceProviderServiceExtensions.cs。

三、核心类型解析:容器是如何运转的

PACKAGE.md 明确列出了本库的三个主要类型,下面逐一结合源码深挖:

3.1 DefaultServiceProviderFactory

实现 DefaultServiceProviderFactory.cs:

public class DefaultServiceProviderFactory : IServiceProviderFactory<IServiceCollection> { private readonly ServiceProviderOptions _options; public DefaultServiceProviderFactory() : this(ServiceProviderOptions.Default) { } public DefaultServiceProviderFactory(ServiceProviderOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); } public IServiceCollection CreateBuilder(IServiceCollection services) => services; public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder) => containerBuilder.BuildServiceProvider(_options); }

它实现了 Abstractions 包中的 IServiceProviderFactory.cs 接口。IServiceProviderFactory<TContainerBuilder>是 .NET Generic Host 与第三方容器(如 Autofac)集成的标准抽象:宿主框架(如Microsoft.Extensions.Hosting)调用CreateBuilder获得容器构建器,应用在其中注册服务,最后宿主调用CreateServiceProvider生成最终的IServiceProvider。默认实现中CreateBuilder直接返回原集合(因为默认容器本身就是IServiceCollection),而CreateServiceProvider则委托给BuildServiceProvider(_options)

3.2 ServiceCollectionContainerBuilderExtensions

定义于 ServiceCollectionContainerBuilderExtensions.cs,提供三个BuildServiceProvider重载:

public static ServiceProvider BuildServiceProvider(this IServiceCollection services) => BuildServiceProvider(services, ServiceProviderOptions.Default); public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes) => services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = validateScopes }); public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(options); return new ServiceProvider(services, options); }

三个重载最终都汇聚到new ServiceProvider(services, options)。注意它返回的是具体类型ServiceProvider而非接口,因此你可以直接使用ServiceProvider上特有的方法(如GetKeyedServiceDisposeAsync)。

3.3 ServiceProvider:默认容器本体

ServiceProvider.cs 是容器的核心实现类:

public sealed class ServiceProvider : IServiceProvider, IKeyedServiceProvider, IDisposable, IAsyncDisposable

它实现了四个接口:

  • IServiceProvider:提供GetService(Type)基础解析能力;
  • IKeyedServiceProvider:支持 .NET 8+ 的键控服务(Keyed Service),可通过GetKeyedService(Type, object? key)按键解析(见 IKeyedServiceProvider.cs);
  • IDisposable/IAsyncDisposable:负责释放容器及所有由它创建的可释放服务。
构造时的关键动作

在 构造函数 中,容器依次完成:

  1. 创建根作用域RootServiceProviderEngineScope),它代表容器的最顶层作用域;
  2. 选择解析引擎GetEngine()——根据运行时是否支持动态代码(RuntimeFeature.IsDynamicCodeSupported)以及Microsoft.Extensions.DependencyInjection.DisableDynamicEngine开关,在表达式树引擎、IL 发射引擎与运行时解释引擎之间选择(详见下文第五节);
  3. 建立 CallSiteFactory:将注册的服务描述符编译成"调用点(CallSite)"工厂;
  4. 注册四类内建服务,即使你没显式注册它们也始终可用:
    • IServiceProviderServiceProviderCallSite
    • IServiceScopeFactory→ 根作用域本身;
    • IServiceProviderIsService/IServiceProviderIsKeyedService→ 用于查询某服务是否已注册(对应 IServiceProviderIsService.cs);
  5. 执行可选校验:若options.ValidateScopes开启,则创建CallSiteValidator用于运行期作用域校验;若options.ValidateOnBuild开启,则在构建时遍历所有描述符逐个验证能否被构造,失败则聚合抛出AggregateException("Some services are not able to be constructed", ...)
解析流程

GetService(Type)内部通过ConcurrentDictionary<ServiceIdentifier, ServiceAccessor>按需编译 + 缓存:同一个服务第一次被请求时,创建并编译其ServiceAccessor,后续请求直接复用缓存的委托,避免重复构建。解析前后还会分别触发OnCreate(调用点校验)与OnResolve(作用域校验),并通过DependencyInjectionEventSource记录ServiceProviderBuiltServiceResolvedServiceProviderDisposed等诊断事件(见 DependencyInjectionEventSource.cs)。

四、生命周期(Lifetime):Singleton / Scoped / Transient

生命周期由 Abstractions 包中的 ServiceLifetime.cs 枚举定义:

生命周期行为典型场景
Singleton整个容器只创建一个实例,首次解析时创建,之后所有请求共享同一实例配置对象、日志器、无状态服务
Scoped每个作用域(Scope)创建一个实例,同一作用域内共享ASP.NET Core 中每个 HTTP 请求一个作用域,故DbContextIUnitOfWork常用 Scoped
Transient每次解析都创建新实例轻量、无状态、线程不安全的短命服务

对应注册方法为AddSingleton<TService, TImpl>()AddScoped<TService, TImpl>()AddTransient<TService, TImpl>(),均定义在 ServiceCollectionServiceExtensions.cs。

4.1 作用域的正确用法

作用域(Scope)通过IServiceScopeFactory创建,最标准的模式是:

using ServiceProvider provider = services.BuildServiceProvider(); using IServiceScope scope = provider.CreateScope(); // 创建子作用域 IMessageWriter writer = scope.ServiceProvider.GetRequiredService<IMessageWriter>();

IServiceScopeFactoryIServiceScope的接口定义分别在 IServiceScopeFactory.cs 和 IServiceScope.cs,其默认实现ServiceProviderEngineScope位于 ServiceLookup/ServiceProviderEngineScope.cs。

重要约束(Captive Dependency 反模式)Singleton服务不能依赖Scoped服务,否则该 Scoped 服务会被"囚禁"在单例中,其作用域语义完全失效。ValidateScopes正是用来在开发期捕获这类错误的(见第五节)。

4.2 释放语义

ServiceProvider实现了IDisposableIAsyncDisposable,其 Dispose 与 DisposeAsync 实现 会级联释放由容器创建的所有IDisposable/IAsyncDisposable实例(容器自行管理的 Singleton 与 Scope 内实例)。特别提醒:

  • 当服务同时实现IAsyncDisposableIDisposable时,同步Dispose()会先调用DisposeAsync并同步等待其完成;
  • 若服务只实现IAsyncDisposable而未实现IDisposable,同步Dispose()会抛出InvalidOperationException,此时必须改用DisposeAsync()——这正是 ServiceProvider.cs 的 XML 注释中"Prefer callingDisposeAsync"的原因。

因此通用最佳实践是:只要容器可能承载异步可释放服务,就优先使用await using+DisposeAsync

五、容器配置:ServiceProviderOptions 与运行期校验

ServiceProviderOptions.cs 是容器的配置入口,包含两个开关:

属性默认值作用
ValidateScopesfalse校验Scoped 服务不会被从根作用域(Root Provider)解析。开启后,一旦从根容器直接解析 Scoped 服务即抛异常,可有效阻止 Captive Dependency
ValidateOnBuildfalseBuildServiceProvider预先验证所有已注册服务能否被成功构造(依赖链是否完整、构造函数是否可满足),失败立即抛出AggregateException开放泛型(Open Generic)服务不参与该校验(见 ServiceProviderOptions.cs 的注释)

注意源码中internal static readonly ServiceProviderOptions Default是一个复用单例,注释明确写着 "Avoid allocating objects in the default case"——默认路径不分配新对象,这也是容器性能优化的一环。

5.1 开启校验的两种方式

// 方式一:bool 重载(开发环境常用) using ServiceProvider provider = services.BuildServiceProvider(validateScopes: true); // 方式二:完整选项(推荐,可同时开启两项) var options = new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true }; using ServiceProvider provider = services.BuildServiceProvider(options);

实践中,ValidateScopes = true通常只应在开发环境开启(ASP.NET Core 的Development环境默认开启),生产环境关闭以省去每层解析前的校验开销。

5.2 作用域校验的底层实现

从 ServiceProvider.cs 可以看到,校验是通过CallSiteValidator挂钩的:OnCreate在调用点(CallSite)构建时执行ValidateCallSiteOnResolve在每次解析时执行ValidateResolution(callSite, scope, Root),传入当前作用域与根作用域做对比。CallSiteValidator的实现位于 ServiceLookup/CallSiteValidator.cs,它会沿服务依赖链回溯,检查是否存在"Scoped 服务被 Singleton 依赖"或"Scoped 服务从根解析"的非法组合。

5.3 宿主中的等价配置

若使用Microsoft.Extensions.Hosting(相关包)而非手动构建容器,上述选项由HostBuilder的配置统一管理,效果与直接传ServiceProviderOptions完全等价——这正是DefaultServiceProviderFactory存在的意义:它把"容器如何构建"从宿主框架中解耦出来,宿主只需要知道IServiceProviderFactory<IServiceCollection>这一抽象即可。

六、解析引擎内幕:三层编译优化架构

本包之所以以高性能著称,源自 ServiceLookup 目录下的多引擎架构。抽象基类 ServiceProviderEngine.cs 只有一个抽象方法:

public abstract Func<ServiceProviderEngineScope, object?> RealizeService(ServiceCallSite callSite);

即在拿到某个服务的"调用点"后,产出一个直接执行解析的委托。三个具体引擎位于同一目录:

  1. Expressions/ExpressionsServiceProviderEngine.cs:基于System.Linq.Expressions表达式树,把解析过程编译成强类型委托;
  2. ILEmit/ILEmitServiceProviderEngine.cs:基于System.Reflection.Emit动态发射 IL,性能最高,但要求运行时支持动态代码;
  3. DynamicServiceProviderEngineRuntimeServiceProviderEngine:前者在支持动态代码时包装编译型引擎,后者是纯解释执行的兜底路径(适用于 AOT、NativeAOT 等禁用动态代码的场景)。

引擎选择的决策点体现在 ServiceProvider.cs:通过AppContext开关Microsoft.Extensions.DependencyInjection.DisableDynamicEngineRuntimeFeature.IsDynamicCodeSupported判定。在 NativeAOT 场景下(仓库中 src/coreclr/nativeaot 支持的目标环境),无法动态发射 IL,容器自动退化为RuntimeServiceProviderEngine,保证功能一致。

围绕引擎工作的是CallSiteFactory(CallSiteFactory.cs):它负责把每个ServiceDescriptor展开为对应的调用点类型——构造函数调用(ConstructorCallSite)、工厂委托(FactoryCallSite)、常量(ConstantCallSite)、IEnumerable<T>批量注入(IEnumerableCallSite)以及ServiceProvider本身(ServiceProviderCallSite),并用CallSiteChain检测循环依赖(CallSiteChain.cs)。ServiceCallSite的类层次定义于 ServiceCallSite.cs。

七、键控服务(Keyed Services):.NET 8+ 的按名解析

ServiceProvider实现了IKeyedServiceProvider,意味着本容器原生支持键控服务。注册与解析的扩展方法分布在 Abstractions 包的键控专用文件中:

  • 注册:AddKeyedSingleton<TService, TImpl>(object? serviceKey)AddKeyedScopedAddKeyedTransient,见 ServiceCollectionServiceExtensions.Keyed.cs;
  • 解析:GetKeyedService<T>(object? serviceKey)GetRequiredKeyedService<T>(object? serviceKey),见 ServiceProviderKeyedServiceExtensions.cs;
  • 注入:构造函数参数上加[FromKeyedServices("key")]特性(FromKeyedServicesAttribute.cs),或用KeyedService.AnyKey一次性解析某类型的全部键控实现。
services.AddKeyedSingleton<IMessageWriter, ConsoleWriter>("console"); services.AddKeyedSingleton<IMessageWriter, FileWriter>("file"); using ServiceProvider provider = services.BuildServiceProvider(); IMessageWriter writer = provider.GetRequiredKeyedService<IMessageWriter>("console");

ServiceIdentifier(ServiceLookup/ServiceIdentifier.cs)将(serviceKey, serviceType)组合作为缓存与解析的键,键控与非键控服务在容器内部统一建模。

八、关联包与生态定位

PACKAGE.md 列出了三个关联包,理清它们的关系有助于理解整个 DI 生态:

  • Microsoft.Extensions.DependencyInjection.Abstractions:纯接口与抽象层(IServiceCollectionIServiceProviderServiceDescriptorServiceLifetime等),不含任何实现。本仓库中其源码位于 src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions,该包自身的 PACKAGE.md 定义了 DI 的抽象契约。
  • Microsoft.Extensions.Hosting:构建带生命周期管理(IHostedService后台任务、日志、配置)的应用程序宿主,其内部正是通过DefaultServiceProviderFactory使用本包作为默认容器;源码位于 src/libraries/Microsoft.Extensions.Hosting。
  • Microsoft.Extensions.Options:基于本容器实现强类型配置绑定(IOptions<T>IOptionsMonitor<T>),把appsettings.json等配置源映射为可注入的服务。

这三者构成 .NET 应用"配置 + DI + 托管"的黄金三角,而本包就是其中最核心的"装配工厂"。

九、总结:本包在仓库中的位置与工程实践要点

Microsoft.Extensions.DependencyInjection包源码位于 src/libraries/Microsoft.Extensions.DependencyInjection/src,作为 .NET 运行时仓库(README 位于 README.md)的一部分,它随 .NET 一起开源(MIT 许可,见 LICENSE.TXT),与System.Private.CoreLib等基础库共同构成 .NET 平台。

工程实践要点速查:

  1. 接口与实现分离:业务代码只依赖 Abstractions 中的接口,替换容器(如换成第三方容器)无需改业务代码;
  2. 生命周期选择:无状态共享用Singleton,请求级状态用Scoped,轻量短命对象用Transient
  3. 开发期开启ValidateScopes = true,快速暴露 Captive Dependency 与作用域滥用;
  4. 生产环境默认配置BuildServiceProvider()ServiceProviderOptions.Default,零额外分配与校验开销;
  5. 优先DisposeAsync:只要容器中可能存在异步可释放服务,就用await using
  6. 理解多引擎架构:动态代码可用时容器自动选择表达式树/IL 发射引擎获得最佳性能,AOT 场景自动降级为解释引擎,无需手动干预。

本文所有源码结论均可在仓库对应路径中找到原始实现,建议读者结合 ServiceProvider.cs、ServiceCollectionContainerBuilderExtensions.cs 与 ServiceLookup 目录继续深入阅读,理解一个生产级 DI 容器的完整设计。

  • 语言运行时
  • 标准库
  • JIT编译
  • 编译器

【免费下载链接】runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

项目地址:https://gitcode.com/GitHub_Trending/runtime6/runtime
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询