ASP.NET Core:IOCDI,即依赖注入和控制反转

Posted AI大胜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core:IOCDI,即依赖注入和控制反转相关的知识,希望对你有一定的参考价值。

此文只是从中摘录整理下自己感兴趣的部分,以便备忘和方便查找回顾,详见:


整个 ASP.NET Core 框架建立在一个底层的依赖注入框架之上,它使用依赖注入容器提供所需的服务对象。

服务

服务通常由组件提供,组件可以理解成对一些特定功能的实现,为了便于定制,这些组件通常以接口的形式进行标准化。

依赖注入(DI)和控制反转(IoC)

IOC: 它是框架中的一种手段或思想,让流程可复用,可定制。这里的流程是指,按照一定的顺序或逻辑调用已有的一些 API,来完成目标任务。

DI: 它用来对依赖的类型进行初始化,这是一种在类及其依赖关系之间实现控制反转 (IoC) 的技术。

为什么要使用DI,我直接对依赖的类型new一个对象不是也很方便?

参考:https://docs.microsoft.com/zh-cn/dotnet/core/extensions/dependency-injection

控制反转(IoC)介绍

控制反转(IoC)的全名 Inverse of Control,它体现的意思是控制权的转移,即控制权原来在A手中,现在需要B来接管,IoC可视为一种程序设计原则。

先了解IoC的C(Control)究竟指的是怎样一种控制。任何一项任务,不论其大小,基本上都可以分解成相应的步骤,所以任何一项任务的实施都有其固有的流程,而IoC涉及的控制可以理解为针对流程的控制

IoC的本质:IoC是设计框架所采用的一种基本思想,所谓的控制反转就是将应用对流程的控制转移到框架之中(框架提供一个引擎,应用中只需初始化这个引擎并直接启动它即可完成目标任务的整个流程)。

类库(Library)和框架(Framework)的不同之处在于:前者往往只是提供实现某种单一功能的API,而后者则针对一个目标任务对这些单一功能进行编排,从而形成一个完整的流程,并利用一个引擎来驱动这个流程自动执行。

以熟悉的ASP.NET MVC应用开发来说,我们只需要按照约定的规则(比如约定的目录结构和文件与类型命名方式等)定义相应的Controller类型和View文件就可以了。当ASP.NET MVC框架在处理请求的过程中,它会根据路由解析生成参数得到目标Controller的类型,然后自动创建Controller对象并执行它。如果目标Action方法需要呈现一个View,框架会根据预定义的目录约定找到对应的View文件(.cshtml文件),并对它实施动态编译生成对应的类型。当目标View对象创建出来之后,它执行之后生成的HTML会作为响应回复给客户端。可以看出,整个请求流程处处体现了框架Call应用这一法则。

IoC将对流程的控制从应用程序转移到框架之中,框架利用一个引擎驱动整个流程的自动化执行。应用程序无需关心工作流程的细节,它只需要启动这个引擎即可。这个引擎一旦被启动,框架就会完全按照预先编排好的流程进行工作,如果应用程序希望整个流程按照自己希望的方式被执行,需要在启动之前对流程(某一处理环节或步骤)进行定制,比如泛化的流程A、B、C,在应用程序B中可以定制成A、B1、C,即重用AC定制B1。一般来说,框架会以相应的形式提供一系列的扩展点,应用程序通过注册扩展的方式实现对流程某个环节的定制。在引擎被启动之前,应用程序将所需的扩展注册到框架之中。一旦引擎被正常启动,这些注册的扩展会自动参与到整个流程的执行过程中。

总的来说,我们在一个框架的基础上进行应用开发,就相当于在一条调试好的流水线上生产某种商品。我们只需要在相应的环节准备对应的原材料,最终下线的就是我们希望得到的产品。IoC几乎是所有框架均具有的一个固有属性。

总结一下:IoC一方面通过流程控制从应用程序向框架的反转实现了针对流程自身的重用,另一方面通过内置的扩展机制使这个被重用的流程能够自由地被定制,这两个因素决定了框架自身的价值。重用让框架不仅仅是为应用程序提供实现单一功能的API,而是提供一整套可执行的解决方案,可定制则使我们可以为不同的应用程序对框架进行定制,这无疑让框架可以使用到更多的应用之中。

另外,IoC不是一种设计模式,而是一种设计思想或原则,一般来讲,设计模式提供了一种解决某种具体问题的方案,很多设计模式都采用了IoC原则的说法才是正确的。

与IoC紧密相连的几种设计模式

依赖注入是一种对象提供型的设计模式,在这里我们将提供的对象统称为“服务”、“服务对象”或者“服务实例”。在一个采用依赖注入的应用中,我们定义某个类型的时候,只需要直接将它依赖的服务采用相应的方式注入进来就可以了。

注意如果对属性或者方法使用[Injection]这一特性,那么依赖注入容器在调用构造函数创建一个目标对象之后,它会自动为具有依赖的目标属性进行赋值。

ASP.NET Core框架使用的依赖注入框架只支持构造器注入,而不支持属性和方法注入(类似于Startup和中间件基于约定的方法注入除外),但是我们很有可能不知不觉地会按照Service Locator模式来编写我们的代码。从某种意义上讲,当我们在程序中使用IServiceProvider(表示依赖注入容器)来提取某个服务实例的时候,就意味着我们已经在使用Service Locator模式了,所以当我们遇到这种情况下的时候应该多想一想是否一定需要这么做。


接下来,介绍采用IoC原则的几种典型设计模式。

模板方法

模板方法模式与IoC的意图可以说不谋而合,该模式主张将一个可复用的工作流程或者由多个步骤组成的算法定义成模板方法, 组成这个流程或者算法的单一步骤则实现在相应的虚方法之中,模板方法根据预先编排的流程去调用这些虚方法。这些方法均定义在一个类中,我们可以通过派生该类并重写相应的虚方法的方式达到对流程定制的目的。

目的:对一个可复用的工作流程或由多个步骤组成的算法进行封装,以便满足对流程的复用和定制。

形式:定义一个类,类中定义若干虚方法(可在子类中重写),再定义一个模板方法(内部按一定的顺序调用那些虚方法)以便完成工作流程或多个步骤组成的算法。

使用:直接调用模板方法完成默认配置的流程,或者定义个子类重写某个虚方法即定制某个步骤,再调用子类的模板方法(继承而来)即可完成目标步骤或算法。

工厂方法

工厂方法,说白了就是在某个类中定义用来提供所需服务对象的方法,这个方法可以是一个单纯的虚方法,也可以是具有默认实现的虚方法。至于方法声明的返回类型,可以是一个接口或者抽象类,也可以是未封闭(Sealed)的具体类型。作为它的派生类型,可以实现或者重写工厂方法以提供所需的服务对象。

目的:在某个类中定义用来提供所需服务对象的方法,即用于对外提供对象实例。

与IoC结合使用:定义工厂方法的类中定义一个类似模板方法的东西,该模板方法内部调用那些工厂方法,获取一个个步骤所需的服务对象,并按顺序执行

抽象工厂

我们需要定义一个独立的工厂接口或者抽象工厂类,并在其中定义多个工厂方法来提供“同一系列”的多个相关对象。如果希望抽象工厂具有一组默认的“产出”,我们也可以将一个未被封闭类型作为抽象工厂,以虚方法形式定义的工厂方法将默认的对象作为返回值。在具体的应用开发中,我们可以通过实现工厂接口或者继承抽象工厂类(不一定是抽象类)的方式来定义具体工厂类,并利用它来提供一组定制的对象系列。

目的:产生一组服务对象,工厂方法更像是产生单一的服务对象

与工厂方法的区别:工厂方法用来产生单一对象,抽象工厂用来产生一组(多个)相关对象。

依赖注入(DI)

依赖和依赖注入(DI)

从面向对象编程的角度来讲,类型中的字段或者属性是依赖的一种主要体现形式。如果类型A中具有一个类型B的字段或属性,或者方法中用到另一个类型B的方法或属性,那么类型A就对类型B产生了依赖,所以可以将依赖注入简单的理解为一种针对依赖字段或属性的自动化初始化方式。实现依赖注入的方式一般分为三种:构造方法注入、属性注入、方法注入。

依赖注入是一种对象提供型的设计模式。可以将提供的对象统称为:服务、服务对象、服务实例。服务实例的提供应完全交给框架来完成,框架利用一个独立的容器(Container)来提供所需的每个服务实例。我们将这个被框架用来提供服务的容器成为依赖注入容器。依赖注入容器之所以能够按照我们希望的方式来提供所需的服务是因为该容器是根据服务注册信息创建的,服务注册包含提供所需服务实例的所有信息。

服务消费者只需要告诉容器所需服务的类型(接口、抽象类或具体类型),就能根据预先注册的规则得到与之匹配的服务实例。

即DI的使用就是换一种方式来获取目标对象(避免在需要该对象的时候直接new一个),被广泛用于使服务可被应用程序任何位置的代码使用。

依赖注入容器

机器猫的那个四次元口袋就是一个理想的依赖注入容器,大熊只需要告诉哆啦A梦相应的需求,它就能从这个口袋中得到相应的法宝。依赖注入容器亦是如此,服务消费者只需要告诉容器所需服务的类型(一般是一个服务接口或者抽象服务类),就能得到与之匹配的服务实例。我们将这个被框架用来提供服务的容器称为“依赖注入容器”。注意:依赖注入容器的使用者是框架而非应用。

简单说,DI 就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。

依赖注入与Service Locator

Service Locator 是指在编程中直接获取依赖注入容器对象,进而利用该对象获取一些别的服务对象。

区别:前者的使用者是框架,后者的使用者是应用程序。另一个角度看,采用依赖注入模式的应用可以看做将服务推送到依赖注入容器,Service Locator模式下的应用则是利用Service Locator拉取所需的服务。

ASP.NET Core 和 DI

可以先参考下这篇文章:https://www.cnblogs.com/xiaoxiaotank/p/15231883.html

ASP.NET Core 框架本身带有自己的一套DI系统,且有一些默认配置好的抽象类和相应实例的映射关系。

有两种方式可以注册其他类型,

一是在StartUp类中的ConfigureServices方法中实现,即静态映射:

public void ConfigureServices(IServiceCollection services)
        
            services.AddControllersWithViews();
            services.AddTransient<IWeather, Weather>();
        

使用DI系统定义的Addxxx扩展方法来绑定类型。该方法是在IServiceCollection接口上定义的。对于配置抽象类型和其对应的实现类,还有另外几个Addxxx方法。

上述这种方式很好满足了,请求一个抽象类的实例,DI系统就能返上述方法中配置的相关抽象类的实例。但是如果需要根据运行时条件将类型T解析为不同的类型,这时就得用动态解析了,动态解析允许指定一个回调函数来解析依赖。

如果为同一个抽象类型注册了多个具体类型,那么DI容器将返回最后注册的类型的一个实例。如果由于二义性或者参数不兼容,导致无法解析构造函数,那么DI容器将抛出一个异常。

在 ASP.NET Core 依赖注入框架中,我们添加的服务注册保存在 通过 IServiceCollection 接口表示的集合之中,由这个集合创建的依赖注入容器体现为一个 IServiceProvider 对象。

IServiceCollection

  • BuildServiceProvider 扩展方法,用于在添加完相关服务注册后创建出代表依赖注入容器的 IServiceProvider 对象。

IServiceProvider

  • GetService<T> 扩展方法,用于获取服务实例。

  • CreateScope 方法,用于创建代表 Scoped 生命周期的 IServiceScope 对象,该对象的 ServiceProvider 属性就是相当于一个子依赖注入容器。

    对于一个ASP.NET Core应用来说,它具有一个与当前应用绑定代表全局根容器的ISerivceProvider对象。对于处理的每一次请求,ASP.NET Core框架都会利用这个根容器来创建基于当前请求的服务范围( IServiceScope 对象),并利用后者提供的 IserviceProvider 对象来提供请求处理所需的服务实例。请求处理完成之后,创建的服务范围被终结,对应的 IServiceProvider 对象也随之被释放,此时由该 IServiceProvider 对象提供的Scoped服务实例以及实现了IDisposable接口的Transient服务实例得以及时释放 。

ASP.NET Core 中的依赖注入的服务生存期

Transient
瞬时,即每次获取,都是一个全新的服务实例。这种生存期适合轻量级、 无状态的服务。 通过 AddTransient 方法注册暂时性服务。

Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域),即与http请求上下文绑定在一起。通过 AddScoped 方法注册范围内服务。

Singleton
单例,即在整个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。通过 AddSingleton 方法注册单一实例服务。 单一实例服务必须是线程安全的,并且通常在无状态服务中使用

在处理请求的应用中,当应用关闭并释放 ServiceProvider 时,会释放单一实例服务。 由于应用关闭之前不释放内存,因此请考虑单一实例服务的内存使用。

如果Singleton 服务依赖另一个Scoped服务,即Scoped 服务实例被一个 Singleton服务实例引用,这也就意味着 Scoped 服务实例成了一个Singleton服务实例。

服务注册方法

框架提供了适用于特定场景的服务注册扩展方法:


更新于:2023.5.20

ASP.NET Core 依赖注入(DI)

原文:ASP.NET Core 依赖注入(DI)

  ASP.NET Core的底层设计支持和使用依赖注入。ASP.NET Core 应用程序可以利用内置的框架服务将服务注入到启动类的方法中,并且应用程序服务也可以配置注入。由ASP.NET Core 提供的默认服务容器提供了最小功能集,并不是取代其他容器。

  1.浅谈依赖注入

  依赖注入(Dependency injection,DI)是一种实现对象和依赖者之间松耦合的技术,将类用来执行其操作的这些对象以注入的方式提供给该类,而不是直接实例化依赖项或者使用静态引用。一般情况,类会通过构造函数声明器2依赖关系,允许他们遵循显示依赖原则。这种方法称为“构造函数注入”。

  当类的设计使用DI思想时,他们的耦合更加松散,因为他们没有对他们的合作者直接硬编码的依赖。这遵循“依赖倒置原则”,其中指出,高层模块不应该依赖于底层模块:两者都依赖于抽象。

  类要求在他们构造时向其提供抽象(通常是接口),而不是引用特定的实现。提取接口的依赖关系和提供接口的实现作为参数也是“策略设计模式”的一个示例。

  当一个类被用来创建类及其相关的依赖关系时,这个成为容器(containers),或者称为控制反转(Inversion of Control, IoC)容器,或者依赖注入容器。容器本质上是一个工厂,负责提供向它请求的类型的实例。如果一个给定类型声明它具有依赖关系,并且容器已经被配置为其提供依赖关系,那么它将把创建依赖关系作为创建请求实例的一部分。除了创建对象的依赖关系外,容器通常还会管理应用程序中对象的生命周期。

  ASP.NET Core 包含一个默认支持构造函数注入的简单内置容器,ASP.NET 的容器指的是它管理的类型services,可以在Startup类的ConfigureServices方法中配置内置容器的服务。

 

  2. 使用ASP.NET Core提供的服务

  Startup类的ConfigureServices方法负责定义应用程序将使用的服务,包括平台自带的功能,比如,Entity Framework Core 和 ASP.NET Core MVC。除了IServiceCollection提供的几个服务之外,可以使用一些扩展方法(AddDbContext,AddMvc,AddTransient等)向容器添加和注册额外服务:  

public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });


            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddDbContext<AccessManagementContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                providerOptions => providerOptions.EnableRetryOnFailure()));
            services.AddTransient<ICompanyServices, CompanyServices>();

        }

  ASP.NET Core 提供的功能和中间件,遵循约定使用一个单一的AddService扩展方法来注册所有该功能所需的服务。

 

  3.注册自己的服务

  我们可以按照 services.AddTransient<ICompanyServices, CompanyServices>(); 这种写法注册自己的服务。第一个范型类型表示将要从容器中请求的类型(通常是一个接口)。第二个范型类型表示将由容器实例化并且用于完成请求的具体类型。

  AddTransient 方法用于将抽象类型映射到为每一个需要它的对象分别实例化的具体服务。为注册的每一个服务选择合适的生命周期很重要,后面会介绍到。

  

  下面是示例是注册自己的服务:

  1.接口

public interface IAccountServices
    {
        Task<List<AccountViewModel>> GetList();
    }

  2.实现类

public class AccountServices:IAccountServices
    {
        AccessManagementContext _context;
        public AccountServices(AccessManagementContext context)
        {
            _context = context;//在构造函数中注入
        }

        public async Task<List<Account>> GetList()
        {
            try
            {
                var query = _context.Account.ToListAsync();
                 return query ;
            }
            catch (Exception ex)
            {
                return null;
            }

        }
}

  3.在ConfigureServices中注册自定义的服务和EF上下文AccessManagementContext 

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddDbContext<AccessManagementContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                providerOptions => providerOptions.EnableRetryOnFailure()));
            services.AddTransient<IAccountServices,AccountServices>();

        }

  4.在Controller构造函数中依赖注入

public class AccountController : Controller
    {
        private IAccountServices _accountServices;
        public AccountController(IAccountServices accountServices)
        {
            _accountServices = accountServices;
        }
        // GET: Account
        public async Task<ActionResult> Index()
        {
            var vms = await _accountServices.GetList();
            return View(vms);
        }

  

  4.服务的生命周期和注册选项

   ASP.NET 服务生命周期:

      1.Transient 瞬时

        Transient 生命周期服务在他们每次请求时被创建。适合轻量级,无状态的服务。

      2.Scoped 作用域

         Scoped生命周期在每次请求时创建一次。

      3.Singleton 单例

        Singleton 生命周期服务在它们第一次请求时创建,并且每个后续请求使用相同的实例。

 

  服务可以用多种方式在容器中注册,除了之前的注册方法,还可以指定一个工厂,它将被用来创建需要的实例。后面会详细介绍其他的注册方法。

  下面用一个简单的示例介绍每个生命周期:

  1.创建接口:

namespace MVCTest.Interfaces
{
    public interface IOperation
    {
        /// <summary>
        /// 唯一标识
        /// </summary>
        Guid OperationId { get;  }
    }

    public interface IOperationTransient: IOperation
    {
    }

    public interface IOperationScoped : IOperation
    {
    }

    public interface IOperationSingleton : IOperation
    {
    }

    public interface IOperationInstance : IOperation
    {
    }
}

  

  2.实现类

    /// <summary>
    /// 实现所有接口
    /// </summary>
    public class Operation: IOperation, IOperationTransient,
        IOperationScoped, IOperationSingleton, IOperationInstance
    {
        public Operation()
        {
            OperationId = Guid.NewGuid();
        }
        public Operation(Guid operationId)
        {
            if (operationId == null)
            {
                OperationId = Guid.NewGuid();
            }
            OperationId = operationId;
        }

        public Guid OperationId { get; }
    }

  3.注册到容器

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IOperationTransient, Operation>();
            services.AddScoped<IOperationScoped, Operation>();
            services.AddSingleton<IOperationSingleton, Operation>();
            services.AddSingleton<IOperationInstance, Operation>();
            services.AddTransient<OperationServices, OperationServices>();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

  4.上面还注册了 OperationServices ,用来测试单例模式(单例生命周期服务中所有请求使用第一次实例化的服务)和 作用域生命周期服务在每次请求时只创建一次,不管几个地方用到实例

    public class OperationServices
    {
        public IOperationTransient OperationTransient { get;  }
        public IOperationScoped OperationScoped { get;  }
        public IOperationSingleton OperationSingleton { get;  }
        public IOperationInstance OperationInstance { get;  }

        public OperationServices(IOperationTransient operationTransient,
            IOperationScoped operationScoped,
            IOperationSingleton operationSingleton,
            IOperationInstance operationInstance)
        {
            OperationTransient = operationTransient;
            OperationScoped = operationScoped;
            OperationSingleton = operationSingleton;
            OperationInstance = operationInstance;
        }
    }

 

  5.在Controller中使用

    public class OperationController : Controller
    {
        public IOperationTransient OperationTransient { get; }
        public IOperationScoped OperationScoped { get; }
        public IOperationSingleton OperationSingleton { get; }
        public IOperationInstance OperationInstance { get; }
        public OperationServices _operationServices;

        public OperationController(IOperationTransient operationTransient,
            IOperationScoped operationScoped,
            IOperationSingleton operationSingleton,
            IOperationInstance operationInstance,
            OperationServices operationServices)
        {
            OperationTransient = operationTransient;
            OperationScoped = operationScoped;
            OperationSingleton = operationSingleton;
            OperationInstance = operationInstance;
            _operationServices = operationServices;
        }
        // GET: Operation
        public ActionResult Index()
        {
            ViewBag.OperationTransient = OperationTransient;
            ViewBag.OperationScoped = OperationScoped;
            ViewBag.OperationSingleton = OperationSingleton;
            ViewBag.OperationInstance = OperationInstance;
            ViewBag._operationServices = _operationServices;
            return View();
        }
}

  6.Index显示

@{
    ViewData["Title"] = "Index";
}

<div>
    <h1>Controller Operations</h1>
    <h2>OperationTransient: @ViewBag.OperationTransient.OperationId</h2>
    <h2>OperationScoped: @ViewBag.OperationScoped.OperationId</h2>
    <h2>OperationSingleton: @ViewBag.OperationSingleton.OperationId</h2>
    <h2>OperationInstance: @ViewBag.OperationInstance.OperationId</h2>
</div>
<div>
    <h1>Services Operations</h1>
    <h2>OperationTransient: @ViewBag._operationServices.OperationTransient.OperationId</h2>
    <h2>OperationScoped: @ViewBag._operationServices.OperationScoped.OperationId</h2>
    <h2>OperationSingleton: @ViewBag._operationServices.OperationSingleton.OperationId</h2>
    <h2>OperationInstance: @ViewBag._operationServices.OperationInstance.OperationId</h2>
</div>

 

  7.运行结果

    技术图片

    可以看到,单例生命周期服务每一次请求的标识一样。作用域生命周期的服务,在一次请求中使用的同一个实例,第二次请求创建新的实例。

 

  5.请求服务

  来自HttpContext的一次ASP.NET 请求中,可用的服务是通过RequestServices集合公开的。

  请求服务将你配置的服务和请求描述为应用程序的一部分。在子的对象指定依赖之后,这些满足要求的对象可通过查找RequestServices中对应的类型得到,而不是ApplicationServices。

 

  6.设计依赖注入服务

  在自定义的服务中,避免使用静态方法和直接实例化依赖的类型,而是通过依赖注入请求它。(New is Glue)

  如果类有太多的依赖关系被注入时,通常表明你的类试图做的太多(违反了单一职责原则),需要转移一些职责。

  同样,Controller类应该重点关注UI,因此业务逻辑和数据访问等细节应该在其他类中。

 

  7.使用Autofac容器

  Autofac

 

以上是关于ASP.NET Core:IOCDI,即依赖注入和控制反转的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core Web 应用程序系列- 使用ASP.NET Core内置的IoC容器DI进行批量依赖注入

ASP.NET Core Web 应用程序系列- 使用ASP.NET Core内置的IoC容器DI进行批量依赖注入(MVC当中应用)

ASP.NET Core 依赖注入(DI)

ASP.NET Core 依赖注入最佳实践——提示与技巧

ASP.Net Core 使用啥依赖注入框架?

ASP.NET Core 依赖注入基本用法