C#依赖注入中IServiceCollection与IServiceProvider关系解析

14 次阅读

依赖注入(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 对象。

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 中保存的注册信息:

  1. 查找 IUserService 对应的 ServiceDescriptor

  2. 判断服务生命周期

  3. 创建或者获取实例

  4. 返回最终对象

因此,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>();

执行过程:

  1. 创建 ServiceCollection

  2. 添加服务描述

  3. 调用 BuildServiceProvider

  4. 生成服务容器

  5. 使用容器解析服务

在 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核心区别总结

对比项IServiceCollectionIServiceProvider
中文理解服务注册集合服务解析容器
创建时间应用启动时注册完成后
主要方法AddSingleton、AddScoped、AddTransientGetService、GetRequiredService
是否创建对象否是
是否管理生命周期保存配置执行生命周期管理
使用场景注册依赖获取依赖

实际开发中的最佳实践

在 C# 项目中使用依赖注入时,应遵循以下原则:

  1. 使用 IServiceCollection 集中管理服务注册。

  2. 使用构造函数注入替代手动获取服务。

  3. 避免在业务代码中直接依赖 IServiceProvider。

  4. 合理设计服务生命周期。

  5. 不要随意调用 BuildServiceProvider 创建额外容器。

理解 IServiceCollection 和 IServiceProvider 的区别,是掌握 C# 依赖注入机制的重要基础。前者负责构建服务配置,后者负责根据配置生成服务实例,两者通过 BuildServiceProvider 连接,共同完成从服务注册到服务使用的完整流程。