ASP.NET Core依赖注入初识与思考

  • A+
所属分类:.NET技术
摘要

文章首发地址在上一篇中,我们讲述了什么是控制反转(IoC)以及通过哪些方式实现的。这其中,我们明白了,控制反转(IoC) 是一种软件设计的模式,指导我们设计出更优良,更具有松耦合的程序,而具体的实现方式有依赖注入和依赖查找。


文章首发地址

一、前言

在上一篇中,我们讲述了什么是控制反转(IoC)以及通过哪些方式实现的。这其中,我们明白了,控制反转(IoC) 是一种软件设计的模式,指导我们设计出更优良,更具有松耦合的程序,而具体的实现方式依赖注入依赖查找

在上篇实例中,我们通过日志的方式举例说明,其中通过代码创建了一个ILogger的接口,并实现接口实例,基于控制反转的模式,依赖的创建也移交到了外部,但是也发现存在了问题,如果类似存在这样多个接口和实现类,依赖太多,一一创建,没有统一的管理,这反而增加了实际工作量麻烦。

因此我们需要一个可以统一管理系统中所有的依赖的地方,因此,IoC容器诞生了。

容器负责两件事情:

  • 绑定服务与实例之间的关系
  • 获取实例,并管理实例对象生命周期(创建与销毁)

ASP.NET Core依赖注入初识与思考

所以在这一篇中,我们主要讲述Asp.Net Core中内置的IoC容器。

二、说明

在Asp.Net Core中已经为我们集成提供了一个内置的IoC容器,我们可以看到在Startup.csConfigureServices中,涉及到依赖注入的核心组件,一个负责实例注册IServiceCollection和一个负责提供实例IServiceProvider

简单的说就是两步:1. 把实例注册到容器中;2. 从容器中获取实例

而在这其中,IServiceCollection为实现将开发者定义好的实例注册进去提供了三种方法

分别是:

AddTransientAddScopedAddSingleton

而这三种不同实例方法也对应的着三种不同的实例生命周期

三种不同的生命周期:

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

效果如下:

ASP.NET Core依赖注入初识与思考

ASP.NET Core依赖注入初识与思考

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。

其他的思考:

  1. ASP.NET Core中,默认采用了构造函数注入的方式,如果采用属性注入或者方法注入,又该怎么实现?
  2. 一个接口多种实现的时候,我们将多种实现都给注入进了依赖注入容器中,但是在服务调用的时候总是获取到最后注入的那个方法的实现,这时候就在想能不能实现动态的选择使用哪种实现呢?
  3. 一个作用域(Scoped)服务中注入一个瞬时(Transient)服务时,瞬时服务中的值还会每次都变化吗?
  4. 链式注入时,生存期的选择,三种注入方式的权重问题

以上的思考,大家可以说说自己的想法

在后续的篇章中,也会对这些问题,进行深入讨论说明。

六、总结

本篇主要介绍了什么是IoC容器,了解到它是DI构造函注入的框架,它管理着依赖项的生命周期以及映射关系,同时也介绍实践了在ASP.Net Core中,默认提供的内置IoC容器,以及它的实例注册方式和相应的生命周期。

好啦,这篇文章就先讲述到这里吧,在后续篇章中会对ASP.Net Core中使用Autofac 框架实践说明,希望对大家有所帮助。

如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。?