ASP.NET Core依赖注入初识与思考
一、前言
在上一篇中,我们讲述了什么是控制反转(IoC)以及通过哪些方式实现的。这其中,我们明白了,控制反转(IoC) 是一种软件设计的模式,指导我们设计出更优良,更具有松耦合的程序,而具体的实现方式有依赖注入和依赖查找。
在上篇实例中,我们通过日志的方式举例说明,其中通过代码创建了一个ILogger
的接口,并实现接口实例,基于控制反转的模式,依赖的创建也移交到了外部,但是也发现存在了问题,如果类似存在这样多个接口和实现类,依赖太多,一一创建,没有统一的管理,这反而增加了实际工作量麻烦。
因此我们需要一个可以统一管理系统中所有的依赖的地方,因此,IoC容器诞生了。
容器负责两件事情:
- 绑定服务与实例之间的关系
- 获取实例,并管理实例对象生命周期(创建与销毁)
所以在这一篇中,我们主要讲述Asp.Net Core中内置的IoC容器。
二、说明
在Asp.Net Core中已经为我们集成提供了一个内置的IoC容器,我们可以看到在Startup.cs
的ConfigureServices
中,涉及到依赖注入的核心组件,一个负责实例注册的IServiceCollection
和一个负责提供实例的IServiceProvider
简单的说就是两步:1. 把实例注册到容器中;2. 从容器中获取实例
而在这其中,IServiceCollection
为实现将开发者定义好的实例注册进去提供了三种方法。
分别是:
AddTransient 、AddScoped 、AddSingleton
而这三种不同实例方法也对应的着三种不同的实例生命周期。
三种不同的生命周期:
2.1 暂时性
AddTransient
每次在向服务容器进行请求时都会创建新的实例,这种生存期适合轻量级、 无状态的服务。
2.2 作用域内
AddScoped
在每次Web请求时被创建一次实例,生命周期横贯整次请求。
局部单例对象, 在某个局部内是同一个对象(作用域单例,本质是容器单例);一次请求内是一个单例对象,多次请求则多个不同的单例对象。
2.3 单例
AddSingleton
创建单例生命周期服务的情况如下:
- 在首次请求它们时进行创建;
- 或者在向容器直接提供实现实例时由开发人员进行创建。 很少用到此方法。
其后的每一个后续请求都使用同一个实例。如果开发者的应用需要单例服务情景,推荐的做法是交给服务容器来负责单例的创建和生命周期管理,而不是手动实现单例模式然后由开发者在自定义类中进行操作。
不要从单一实例解析指定了作用域的服务。 当处理后续请求时,它可能会导致服务处于不正确的状态。 可以从范围内或暂时性服务解析单一实例服务。
三、开始
3.1 接口
定义三个接口,分别测试Singleton,Scope,Transient三种,一个 TestService服务
public interface ITransientService
{
string GetGuid();
}
public interface IScopedService
{
string GetGuid();
}
public interface ISingletonService
{
string GetGuid();
}
public interface ITestService
{
public string GetSingletonID();
public string GetTransientID();
public string GetScopedID();
}
3.2 实现
根据上面定义的几种接口,并一一实现对应的接口
public class TransientService : ITransientService
{
public string OperationId { get; }
public TransientService()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string GetGuid()
{
return $"这是一个 Transient service : " + OperationId;
}
}
public class ScopedService : IScopedService
{
public string OperationId { get; }
public ScopedService()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string GetGuid()
{
return $"这是一个 scoped service : "+ OperationId;
}
}
public class SingletonService : ISingletonService
{
public string OperationId { get; }
public SingletonService()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string GetGuid()
{
return $"这是一个 Singleton service : " + OperationId;
}
}
public class TestService : ITestService
{
private ITransientService _transientService;
private IScopedService _scopedService;
private ISingletonService _singletonService;
public TestService(ITransientService transientService, IScopedService scopedService, ISingletonService singletonService)
{
_transientService = transientService;
_scopedService = scopedService;
_singletonService = singletonService;
}
public string GetSingletonID()
{
return _singletonService.GetGuid();
}
public string GetTransientID()
{
return _transientService.GetGuid();
}
public string GetScopedID()
{
return _scopedService.GetGuid();
}
}
3.3 注入
在Startup.cs
类文件ConfigureServices
方法中,注入依赖
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddTransient<ITransientService, TransientService>();
services.AddSingleton<ISingletonService, SingletonService>();
services.AddScoped<IScopedService, ScopedService>();
services.AddScoped<ITestService, TestService>();
}
3.4 调用
定义一个控制器,实现调用
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private ITransientService _transientService;
private IScopedService _scopedService;
private ISingletonService _singletonService;
private ITestService _testService;
public TestController(ITransientService transientService, IScopedService scopedService, ISingletonService singletonService,
ITestService testService
)
{
_transientService = transientService;
_scopedService = scopedService;
_singletonService = singletonService;
_testService = testService;
}
[HttpGet]
public JsonResult Get()
{
var data1 = _transientService.GetGuid();
var data2 = _testService.GetTransientID();
var data3 = _scopedService.GetGuid();
var data4 = _testService.GetScopedID();
var data5 = _singletonService.GetGuid();
var data6 = _testService.GetSingletonID();
return new JsonResult(new {
data1,
data2,
data3 ,
data4,
data5,
data6,
});
}
}
在上面中我们了解到,注入的方式一般有三种,构造函数注入, 方法注入,属性注入,而在ASP.NET Core中自带的这个IoC容器,默认采用了构造函数注入的方式。
3.5 测试
启动运行项目,访问接口/Test
效果如下:
3.6 对比
对比两次的请求访问可以发现,上面我们一共得到了 4个Transient实例,2个Scope实例,1个Singleton实例。
在请求中,AddSingleton方式的id值相同;
AddScope方式两次请求之间不同,但同一请求内是相同的;
AddTransient方式在同一请求内的多次注入间都不相同。
3.7小结
通过上述的代码示例,也证实了之前的三种方式对应不同生命周期的说明。
暂时性(Transient) : 生命周期是每次获得对象都是一次新的对象,每一次都不一样。
作用域内(Scoped) : 生命周期是在每一次请求作用域内是同一个对象,非作用域内则是新的对象。
单例(Singletion) : 生命周期是这个服务启动后都是一个对象,也即是全局单例对象
。
四、其他Ioc容器
通过上述的了解,我们知道IoC容器是一个依赖注入框架,在.NET Core中也提供了内置的IoC容器,通过AddXXX方法来实例依赖对象,而在实际开发中,就是每一个实例都需要一个个的添加,这样的实现方式在小项目中还好,但是如果在复杂大型的项目中,就略向麻烦些,可能需要添加很多方法来实现,整体的可观性也不好。
为了达到可以简化我们工作量,应该采用批量注册,因此我们也可以引入其他的Ioc容器框架,实现更多的功能和扩展。
在平时开发中,常用的IoC框架有很多,而在这里我们选择用Autofac,这也是在.net下比较流行的,其他的框架不做说明,可自行查阅了解。
ASP.Net Core中使用Autofac 框架注入 (在后续篇章会具体说明)
五、思考
在实际的开发中,对于这几种生命周期,我们应该如何应用呢?
比如,在像DBContext这种实例,在实际开发中是用Transient 还是Scoped呢?
建议使用Scoped
因为有些对象在请求中可以需要用到多个方法或者多个Service、Repository的时候,为了减少实例初始化的消耗,实现事务功能,可以在整个请求的生命周期共用一个Scope。
其他的思考:
- ASP.NET Core中,默认采用了构造函数注入的方式,如果采用属性注入或者方法注入,又该怎么实现?
- 一个接口多种实现的时候,我们将多种实现都给注入进了依赖注入容器中,但是在服务调用的时候总是获取到最后注入的那个方法的实现,这时候就在想能不能实现动态的选择使用哪种实现呢?
- 一个作用域(Scoped)服务中注入一个瞬时(Transient)服务时,瞬时服务中的值还会每次都变化吗?
- 链式注入时,生存期的选择,三种注入方式的权重问题
以上的思考,大家可以说说自己的想法
在后续的篇章中,也会对这些问题,进行深入讨论说明。
六、总结
本篇主要介绍了什么是IoC容器,了解到它是DI构造函注入的框架,它管理着依赖项的生命周期以及映射关系,同时也介绍实践了在ASP.Net Core中,默认提供的内置IoC容器,以及它的实例注册方式和相应的生命周期。
好啦,这篇文章就先讲述到这里吧,在后续篇章中会对ASP.Net Core中使用Autofac 框架实践说明,希望对大家有所帮助。
如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。🤣