.NET依赖注入

什么是依赖注入

依赖注入是一种设计模式,它让类的依赖项(比如服务、数据库、日志组件等)由外部系统(比如框架)提供,而不是类自己创建。

常见术语

Service: 被注入的服务(类、接口)

Container: 注册和管理服务的容器

Lifetime: 服务的生命周期(Scoped、Singleton、Transient)

服务生命周期

依赖注入容器管理服务的生命周期(Lifetime),决定了服务实例的创建和销毁时机。

Scoped(作用域)

定义:在一个请求(Scope)中创建一个实例。通常在 Web 应用中表示一次 HTTP 请求周期。

注册方式:services.AddScoped<TService, TImplementation>()

适用场景:适用于需要在一个请求中保持状态,但请求之间不共享的服务,例如工作单元(Unit of Work)。

比喻:

1
services.AddScoped<IMyService, MyService>();

说明:在同一次 HTTP 请求中注入的都是同一个 MyService 实例,不同请求是不同实例。

想象你是一个饭店老板:

每进来一个顾客(表示一次 HTTP 请求):

给他分配一个【托盘】(表示一个作用域 Scope)。

然后在托盘上放饮料、菜单(服务对象)。

Scoped 生命周期就像【每个顾客都有自己的托盘,托盘里的服务对这个顾客是一样的】:

顾客 A(HTTP 请求 1)来了,系统给他一个新的 MyService 实例。

顾客 A 用了一整顿饭(这个请求过程中),无论 Controller / Service 层调用几次,拿到的都是这个实例。

顾客 B(HTTP 请求 2)又来了,他也分配了一个新的 MyService 实例,跟 A 的不同。

例子

在 ASP.NET Core Web 项目中使用它来验证“同一个请求用同一个实例”的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IMyScopedService
{
Guid GetOperationId();
}

public class MyScopedService : IMyScopedService
{
private readonly Guid _operationId;

public MyScopedService()
{
_operationId = Guid.NewGuid();
}

public Guid GetOperationId()
{
return _operationId;
}
}

注册服务到容器中 Program.cs

1
builder.Services.AddScoped<IMyScopedService, MyScopedService>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly IMyScopedService _service1;
private readonly IMyScopedService _service2;

public TestController(IMyScopedService service1, IMyScopedService service2)
{
_service1 = service1;
_service2 = service2;
}

[HttpGet("check")]
public IActionResult CheckScoped()
{
var id1 = _service1.GetOperationId();
var id2 = _service2.GetOperationId();

return Ok(new
{
Service1Id = id1,
Service2Id = id2,
AreSame = id1 == id2
});
}
}

测试效果

启动项目,访问 /test/check

会看到输出如下(两个 ID 一样,说明是同一个实例):

1
2
3
4
5
{
"Service1Id": "0b6a22e2-2a8d-4896-9d59-82452f7032e6",
"Service2Id": "0b6a22e2-2a8d-4896-9d59-82452f7032e6",
"AreSame": true
}

刷新页面(即另一个请求),会得到新的 ID:

1
2
3
4
5
{
"Service1Id": "2e191177-c94a-42ce-b274-1faad4f5a5e2",
"Service2Id": "2e191177-c94a-42ce-b274-1faad4f5a5e2",
"AreSame": true
}

说明:

每次请求都会创建新的 MyScopedService 实例。

同一个请求内的多个注入是同一个实例

Singleton(单例)

定义:整个应用程序生命周期中只创建一个实例,并被所有请求共享。

注册方式:services.AddSingleton<TService, TImplementation>()

适用场景:线程安全、无状态服务,或有状态但共享全局状态的服务。

比喻理解 Singleton 生命周期(单例)
饭店老板的例子(继续)
还记得之前说的“每个顾客一个托盘”是 Scoped 吗?
现在换成 Singleton:
就像你饭店里有一台共享的饮水机(或菜单看板)☕,所有顾客都用同一个,不会为每个顾客新建一个。
技术上:Singleton 就是 “只创建一次服务实例”,并在整个程序中共享。

1
services.AddSingleton<IMyService, MyService>();

说明:第一次请求时创建 MyService 的实例,之后每次都使用同一个实例。

例子

和之前的 Scoped 示例相比,我们只改注册方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IMySingletonService
{
Guid GetOperationId();
}

public class MySingletonService : IMySingletonService
{
private readonly Guid _operationId;

public MySingletonService()
{
_operationId = Guid.NewGuid();
}

public Guid GetOperationId()
{
return _operationId;
}
}

注册为 Singleton(Program.cs)

1
builder.Services.AddSingleton<IMySingletonService, MySingletonService>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly IMySingletonService _service1;
private readonly IMySingletonService _service2;

public TestController(IMySingletonService service1, IMySingletonService service2)
{
_service1 = service1;
_service2 = service2;
}

[HttpGet("singleton")]
public IActionResult CheckSingleton()
{
var id1 = _service1.GetOperationId();
var id2 = _service2.GetOperationId();

return Ok(new
{
Service1Id = id1,
Service2Id = id2,
AreSame = id1 == id2
});
}
}

访问测试:
打开浏览器访问 /test/singleton:

1
2
3
4
5
{
"Service1Id": "f8e3a16e-1c6f-4531-9ed3-2bb89f4f750a",
"Service2Id": "f8e3a16e-1c6f-4531-9ed3-2bb89f4f750a",
"AreSame": true
}

再次刷新页面,你会发现输出的还是同一个 ID → 说明所有请求用的是同一个服务实例

Transient(瞬态)

定义:每次请求都会创建新的实例。

注册方式:services.AddTransient<TService, TImplementation>()

适用场景:轻量级、无状态服务;适合短生命周期的对象。其缺点是生成的对象比较多,会浪费内存。

示例:

1
services.AddTransient<IMyService, MyService>();

说明:每次注入时都创建一个新的 MyService 实例。

生命周期的适配策略

场景 推荐生命周期
全局配置服务 Singleton
数据库上下文(DbContext) Scoped
工具类/轻量服务 Transient
缓存服务(如 Redis 缓存) Singleton
日志记录器 Singleton
用户请求上下文相关服务 Scoped

注意事项
不要在 Singleton 中注入 Scoped 或 Transient
会导致作用域错误或内存泄漏。
如果一定要这么做,可以使用 IServiceScopeFactory 手动创建作用域。

EF Core 的 DbContext 默认是 Scoped 生命周期
所以应避免把 DbContext 注入到 Singleton 服务中。

比喻理解 Transient(瞬态)
继续用饭店的例子:

Transient 就像你给顾客倒水时每次都换一个一次性纸杯,谁要用水,立即临时给他一个新的杯子,用完就扔掉,永远不会复用

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IMyTransientService
{
Guid GetOperationId();
}

public class MyTransientService : IMyTransientService
{
private readonly Guid _operationId;

public MyTransientService()
{
_operationId = Guid.NewGuid();
}

public Guid GetOperationId()
{
return _operationId;
}
}

注册为 Transient(Program.cs)

1
builder.Services.AddTransient<IMyTransientService, MyTransientService>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly IMyTransientService _service1;
private readonly IMyTransientService _service2;

public TestController(IMyTransientService service1, IMyTransientService service2)
{
_service1 = service1;
_service2 = service2;
}

[HttpGet("transient")]
public IActionResult CheckTransient()
{
var id1 = _service1.GetOperationId();
var id2 = _service2.GetOperationId();

return Ok(new
{
Service1Id = id1,
Service2Id = id2,
AreSame = id1 == id2
});
}
}
1
2
3
4
5
{
"Service1Id": "a12b1234-bbcd-4f9a-81a3-f93dabc56ab1",
"Service2Id": "9f334b12-fada-4873-bc6d-ff3455c68e92",
"AreSame": false
}

注意:同一个请求中注入两次服务,拿到的是两个不同实例!
再刷新页面,ID 还会变 → 说明每次请求都会 new。

三种生命周期对比总结

生命周期 创建频率 使用范围 举例类比

Singleton 应用启动时创建一次 所有地方都复用 饮水机 / 菜单看板

Scoped 每次请求创建一次 当前请求中复用 每人托盘

Transient 每次注入都创建新实例 无复用,每次都 new 一次性纸杯