依赖注入(Dependency Injection,简称 DI)是现代 C# 应用开发中非常重要的设计模式,尤其是在 ASP.NET Core 项目中,依赖注入已经成为默认的服务管理方式。开发者在使用依赖注入时,经常会接触到两个核心接口:IServiceCollection 和 IServiceProvider。
很多初学者容易混淆这两个接口的职责,认为它们都是用于获取服务的组件。实际上,IServiceCollection 负责“注册服务”,而 IServiceProvider 负责“提供服务”。二者处于依赖注入生命周期的不同阶段,共同完成整个服务管理流程。
理解 IServiceCollection 与 IServiceProvider 的关系,对于掌握 ASP.NET Core 启动流程、服务生命周期管理以及高级依赖注入技巧非常重要。
IServiceCollection是什么
IServiceCollection 是一个服务集合接口,本质上是用于保存服务注册信息的容器。
它定义在 Microsoft.Extensions.DependencyInjection 命名空间中:
C#public interface IServiceCollection : IList<ServiceDescriptor> { }
可以看到,IServiceCollection 实际上继承自 IList
ServiceDescriptor 描述了一个服务注册信息,包括:
-
服务类型(Service Type)
-
实现类型(Implementation Type)
-
生命周期(Lifetime)
-
实例对象(Implementation Instance)
-
工厂方法(Implementation Factory)
例如:
C#services.AddTransient<IUserService, UserService>();
执行这段代码时,并不会立即创建 UserService 对象,而是在 IServiceCollection 中添加一条服务描述:
接口类型:IUserService 实现类型:UserService 生命周期:Transient
此时只是完成了服务注册。
IServiceProvider是什么
IServiceProvider 是服务提供者接口,它负责根据服务类型创建和返回对应实例。
接口定义如下:
C#public interface IServiceProvider { object? GetService(Type serviceType); }
常见使用方式:
C#var userService = serviceProvider.GetService<IUserService>();
当调用 GetService 方法时,IServiceProvider 会根据之前 IServiceCollection 中保存的注册信息:
-
查找 IUserService 对应的 ServiceDescriptor
-
判断服务生命周期
-
创建或者获取实例
-
返回最终对象
因此,IServiceProvider 才是真正负责解析依赖的对象。
简单理解:
-
IServiceCollection:记录“如何创建服务”
-
IServiceProvider:按照记录“创建服务”
IServiceCollection与IServiceProvider的关系
两者之间存在明显的上下游关系:
IServiceCollection | | BuildServiceProvider() ↓ IServiceProvider | | GetService() ↓ 具体服务实例
完整流程如下:
第一步:注册服务
应用启动时:
C#var services = new ServiceCollection(); services.AddScoped<IOrderService, OrderService>(); services.AddSingleton<IConfigService, ConfigService>();
此时 IServiceCollection 中保存:
IOrderService -> OrderService 生命周期 Scoped IConfigService -> ConfigService 生命周期 Singleton
第二步:构建服务提供者
通过:
C#var provider = services.BuildServiceProvider();
创建 IServiceProvider。
BuildServiceProvider 会读取 IServiceCollection 中所有注册信息,并创建一个能够解析服务的容器。
第三步:解析服务
通过 IServiceProvider 获取实例:
C#var orderService = provider.GetService<IOrderService>();
此时容器会根据注册信息创建 OrderService。
BuildServiceProvider的作用
BuildServiceProvider 是连接 IServiceCollection 和 IServiceProvider 的关键方法。
示例:
C#IServiceCollection services = new ServiceCollection(); services.AddTransient<TestService>(); IServiceProvider provider = services.BuildServiceProvider(); var service = provider.GetService<TestService>();
执行过程:
-
创建 ServiceCollection
-
添加服务描述
-
调用 BuildServiceProvider
-
生成服务容器
-
使用容器解析服务
在 ASP.NET Core 中,框架已经自动完成了 BuildServiceProvider 的过程。
例如:
C#public void ConfigureServices(IServiceCollection services) { services.AddControllers(); }
这里传入的 services 就是 IServiceCollection。
应用启动后,ASP.NET Core 会自动创建对应的 IServiceProvider。
ASP.NET Core中的实际关系
在 ASP.NET Core 项目中,依赖注入流程如下:
Program.cs ↓ IServiceCollection ↓ 注册各种服务 ↓ WebApplicationBuilder ↓ 创建IServiceProvider ↓ Controller/Middleware获取服务
例如:
C#builder.Services.AddScoped<IProductRepository, ProductRepository>(); var app = builder.Build();
其中:
C#builder.Services
就是 IServiceCollection。
而:
C#app.Services
则是 IServiceProvider。
例如:
C#var service = app.Services.GetService<IProductRepository>();
通过 IServiceProvider 获取已经注册的服务。
IServiceCollection负责配置,IServiceProvider负责运行
可以将二者职责进行简单区分:
| 接口 | 主要职责 | 使用阶段 |
|---|---|---|
| IServiceCollection | 添加服务注册信息 | 应用启动阶段 |
| IServiceProvider | 创建和获取服务实例 | 应用运行阶段 |
例如:
注册阶段:
C#services.AddSingleton<ICache, MemoryCache>();
运行阶段:
C#var cache = provider.GetService<ICache>();
前者是配置,后者是使用。
三种生命周期在IServiceProvider中的表现
IServiceProvider 不仅负责创建对象,还负责维护服务生命周期。
Transient瞬时服务
每次请求都会创建新实例:
C#services.AddTransient<IEmailService, EmailService>();
获取:
C#var a = provider.GetService<IEmailService>(); var b = provider.GetService<IEmailService>();
结果:
a != b
适用于无状态轻量服务。
Scoped作用域服务
同一个作用域内共享实例:
C#services.AddScoped<IUserContext, UserContext>();
ASP.NET Core 中通常对应一次 HTTP 请求。
例如:
请求1: UserContext对象A 请求2: UserContext对象B
适用于数据库上下文等场景。
Singleton单例服务
整个应用生命周期只创建一次:
C#services.AddSingleton<ISettingService, SettingService>();
所有地方获取:
对象地址相同
适用于配置、缓存等全局服务。
为什么不建议手动创建IServiceProvider
有些开发者会在业务代码中这样写:
C#var provider = services.BuildServiceProvider();
虽然可以运行,但通常不推荐。
原因包括:
1. 创建多个容器
每次 BuildServiceProvider 都会生成新的容器:
C#var provider1 = services.BuildServiceProvider(); var provider2 = services.BuildServiceProvider();
两个容器之间生命周期完全独立。
可能导致:
-
Singleton对象重复创建
-
资源无法正确释放
2. 生命周期管理混乱
依赖注入框架负责管理对象释放。
手动创建 Provider 后,需要自己处理:
C#provider.Dispose();
否则可能造成资源泄漏。
3. 隐藏依赖关系
如果业务代码大量依赖:
C#IServiceProvider
会变成服务定位器模式(Service Locator),降低代码可维护性。
推荐通过构造函数注入:
C#public class OrderController { private readonly IOrderService service; public OrderController(IOrderService service) { this.service = service; } }
IServiceProvider中的GetRequiredService
除了 GetService,还经常使用:
C#provider.GetRequiredService<IUserService>();
区别:
GetService:
C#var service = provider.GetService<IUserService>();
如果不存在:
返回null
GetRequiredService:
C#var service = provider.GetRequiredService<IUserService>();
如果不存在:
直接抛出异常
例如:
InvalidOperationException: No service for type IUserService has been registered.
在关键业务服务获取时,更推荐使用 GetRequiredService。
如何查看IServiceCollection中的注册信息
由于 IServiceCollection 本质是集合,可以直接遍历:
C#foreach(var service in services) { Console.WriteLine(service.ServiceType); Console.WriteLine(service.ImplementationType); }
输出类似:
IUserService UserService IOrderService OrderService
这对于调试依赖注入问题非常有帮助。
常见错误分析
错误一:忘记注册服务
代码:
C#public UserController(IUserService service) { }
但是没有:
C#services.AddScoped<IUserService, UserService>();
运行时:
Unable to resolve service for type IUserService
解决方法:
检查 IServiceCollection 是否完成注册。
错误二:生命周期依赖错误
例如:
C#services.AddSingleton<OrderService>(); services.AddScoped<DbContext>();
Singleton 服务依赖 Scoped 服务,会导致生命周期冲突。
原因:
单例对象生命周期长于作用域对象。
解决方法:
调整生命周期设计。
错误三:重复调用BuildServiceProvider
错误:
C#var provider = services.BuildServiceProvider();
在 ConfigureServices 中频繁调用。
解决方法:
让框架统一管理 IServiceProvider。
IServiceCollection与IServiceProvider核心区别总结
| 对比项 | IServiceCollection | IServiceProvider |
|---|---|---|
| 中文理解 | 服务注册集合 | 服务解析容器 |
| 创建时间 | 应用启动时 | 注册完成后 |
| 主要方法 | AddSingleton、AddScoped、AddTransient | GetService、GetRequiredService |
| 是否创建对象 | 否 | 是 |
| 是否管理生命周期 | 保存配置 | 执行生命周期管理 |
| 使用场景 | 注册依赖 | 获取依赖 |
实际开发中的最佳实践
在 C# 项目中使用依赖注入时,应遵循以下原则:
-
使用 IServiceCollection 集中管理服务注册。
-
使用构造函数注入替代手动获取服务。
-
避免在业务代码中直接依赖 IServiceProvider。
-
合理设计服务生命周期。
-
不要随意调用 BuildServiceProvider 创建额外容器。
理解 IServiceCollection 和 IServiceProvider 的区别,是掌握 C# 依赖注入机制的重要基础。前者负责构建服务配置,后者负责根据配置生成服务实例,两者通过 BuildServiceProvider 连接,共同完成从服务注册到服务使用的完整流程。