理解ASP.NET Core 依赖注入(Dependency Injection)

目录
  • 依赖注入
    • 什么是依赖注入
    • 依赖注入有什么好处
  • ASP.NET Core内置的依赖注入
    • 服务生存周期
    • 服务释放
    • TryAdd{Lifetime}扩展方法
    • 解析同一服务的多个不同实现
    • Replace && Remove 扩展方法
  • Autofac
  • 服务解析和注入
    • 构造函数注入
    • 方法注入
    • 属性注入
  • 一些注意事项
  • 框架默认提供的服务

依赖注入

什么是依赖注入

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

依赖注入有什么好处

依赖注入在.NET中,可谓是“一等公民”,处处都离不开它,那么它有什么好处呢?

假设有一个日志类 FileLogger,用于将日志记录到本地文件。

public class FileLogger
{
    public void LogInfo(string message)
    {

    }
}

日志很常用,几乎所有服务都需要记录日志。如果不使用依赖注入,那么我们就必须在每个服务中手动 new FileLogger 来创建一个 FileLogger 实例。

public class MyService
{
    private readonly FileLogger _logger = new FileLogger();

    public void Get()
    {
        _logger.LogInfo("MyService.Get");
    }
}

如果某一天,想要替换掉 FileLogger,而是使用 ElkLogger,通过ELK来处理日志,那么我们就需要将所有服务中的代码都要改成 new ElkLogger。

public class MyService
{
    private readonly ElkLogger _logger = new ElkLogger();

    public void Get()
    {
        _logger.LogInfo("MyService.Get");
    }
}
  • 在一个大型项目中,这样的代码分散在项目各处,涉及到的服务均需要进行修改,显然一个一个去修改不现实,且违反了“开闭原则”。
  • 如果Logger中还需要其他一些依赖项,那么用到Logger的服务也要为其提供依赖,如果依赖项修改了,其他服务也必须要进行更改,更加增大了维护难度。
  • 很难进行单元测试,因为它无法进行 mock

正因如此,所以依赖注入解决了这些棘手的问题:

  • 通过接口或基类(包含抽象方法或虚方法等)将依赖关系进行抽象化
  • 将依赖关系存放到服务容器中
  • 由框架负责创建和释放依赖关系的实例,并将实例注入到构造函数、属性或方法中

ASP.NET Core内置的依赖注入

服务生存周期

Transient
瞬时,即每次获取,都是一个全新的服务实例

Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。

Singleton
单例,即在单个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。

服务释放

若服务实现了IDisposable接口,并且该服务是由DI容器创建的,那么你不应该去Dispose,DI容器会对服务自动进行释放。

如,有Service1、Service2、Service3、Service4四个服务,并且都实现了IDisposable接口,如:

public class Service1 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service1.Dispose");
    }
}

public class Service2 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service2.Dispose");
    }
}

public class Service3 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service3.Dispose");
    }
}

public class Service4 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service4.Dispose");
    }
}

并注册为:

public void ConfigureServices(IServiceCollection services)
{
    // 每次使用完(请求结束时)即释放
    services.AddTransient<Service1>();
    // 超出范围(请求结束时)则释放
    services.AddScoped<Service2>();
    // 程序停止时释放
    services.AddSingleton<Service3>();
    // 程序停止时释放
    services.AddSingleton(sp => new Service4());
}

构造函数注入一下

public ValuesController(
    Service1 service1,
    Service2 service2,
    Service3 service3,
    Service4 service4)
{ }

请求一下,获取输出:

Service2.Dispose
Service1.Dispose

这些服务实例都是由DI容器创建的,所以DI容器也会负责服务实例的释放和销毁。注意,单例此时还没到释放的时候。

但如果注册为:

public void ConfigureServices(IServiceCollection services)
{
    // 注意与上面的区别,这个是直接 new 的,而上面是通过 sp => new 的
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());
    services.AddSingleton(new Service3());
    services.AddSingleton(new Service4());
}

此时,实例都是咱们自己创建的,DI容器就不会负责去释放和销毁了,这些工作都需要我们开发人员自己去做。

更多注册方式,请参考官方文档-Service registration methods

TryAdd{Lifetime}扩展方法

当你将同样的服务注册了多次时,如:

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

那么当使用IEnumerable<{Service}>(下面会讲到)解析服务时,就会产生多个MyService实例的副本。

为此,框架提供了TryAdd{Lifetime}扩展方法,位于命名空间Microsoft.Extensions.DependencyInjection.Extensions下。当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。

services.AddTransient<IMyService, MyService1>();
// 由于上面已经注册了服务类型 IMyService,所以下面的代码不不会执行任何操作(与生命周期无关)
services.TryAddTransient<IMyService, MyService1>();
services.TryAddTransient<IMyService, MyService2>();
  • TryAdd:通过参数ServiceDescriptor将服务类型、实现类型、生命周期等信息传入进去
  • TryAddTransient:对应AddTransient
  • TryAddScoped:对应AddScoped
  • TryAddSingleton:对应AddSingleton
  • TryAddEnumerable:这个和TryAdd的区别是,TryAdd仅根据服务类型来判断是否要进行注册,而TryAddEnumerable则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。举个例子吧:
// 注册了 IMyService - MyService1
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
// 注册了 IMyService - MyService2
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService2>());
// 未进行任何操作,因为 IMyService - MyService1 在上面已经注册了
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());

解析同一服务的多个不同实现

默认情况下,如果注入了同一个服务的多个不同实现,那么当进行服务解析时,会以最后一个注入的为准。

如果想要解析出同一服务类型的所有服务实例,那么可以通过IEnumerable<{Service}>来解析(顺序同注册顺序一致):

public interface IAnimalService { }
public class DogService : IAnimalService { }
public class PigService : IAnimalService { }
public class CatService : IAnimalService { }

public void ConfigureServices(IServiceCollection services)
{
    // 生命周期没有限制
    services.AddTransient<IAnimalService, DogService>();
    services.AddScoped<IAnimalService, PigService>();
    services.AddSingleton<IAnimalService, CatService>();
}

public ValuesController(
    // CatService
    IAnimalService animalService,
    // DogService、PigService、CatService
    IEnumerable<IAnimalService> animalServices)
{
}

Replace && Remove 扩展方法

上面我们所提到的,都是注册新的服务到DI容器中,但是有时我们想要替换或是移除某些服务,这时就需要使用ReplaceRemove

// 将 IMyService 的实现替换为 MyService1
services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 注册的实现 MyService
services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 的所有注册
services.RemoveAll<IMyService>();
// 清除所有服务注册
services.Clear();

Autofac

Autofac 是一个老牌DI组件了,接下来我们使用Autofac替换ASP.NET Core自带的DI容器。

1.安装nuget包:

Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection

2.替换服务提供器工厂

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        // 通过此处将默认服务提供器工厂替换为 autofac
        .UseServiceProviderFactory(new AutofacServiceProviderFactory());

3.在 Startup 类中添加 ConfigureContainer 方法

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public ILifetimeScope AutofacContainer { get; private set; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 1. 不要 build 或返回任何 IServiceProvider,否则会导致 ConfigureContainer 方法不被调用。
        // 2. 不要创建 ContainerBuilder,也不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了
        // 3. 你仍然可以在此处通过微软默认的方式进行服务注册

        services.AddOptions();
        services.AddControllers();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication.Ex", Version = "v1" });
        });
    }

    // 1. ConfigureContainer 用于使用 Autofac 进行服务注册
    // 2. 该方法在 ConfigureServices 之后运行,所以这里的注册会覆盖之前的注册
    // 3. 不要 build 容器,不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了
    public void ConfigureContainer(ContainerBuilder builder)
    {
        // 将服务注册划分为模块,进行注册
        builder.RegisterModule(new AutofacModule());
    }

    public class AutofacModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            // 在此处进行服务注册
            builder.RegisterType<UserService>().As<IUserService>();
        }
    }

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {
        // 通过此方法获取 autofac 的 DI容器
        AutofacContainer = app.ApplicationServices.GetAutofacRoot();
    }
}

服务解析和注入

上面我们主要讲了服务的注入方式,接下来看看服务的解析方式。解析方式有两种:

1.IServiceProvider

2.ActivatorUtilities

  • 用于创建未在DI容器中注册的服务实例
  • 用于某些框架级别的功能

构造函数注入

上面我们举得很多例子都是使用了构造函数注入——通过构造函数接收参数。构造函数注入是非常常见的服务注入方式,也是首选方式,这要求:

  • 构造函数可以接收非依赖注入的参数,但必须提供默认值
  • 当服务通过IServiceProvider解析时,要求构造函数必须是public
  • 当服务通过ActivatorUtilities解析时,要求构造函数必须是public,虽然支持构造函数重载,但必须只能有一个是有效的,即参数能够全部通过依赖注入得到值

方法注入

顾名思义,方法注入就是通过方法参数来接收服务实例。

[HttpGet]
public string Get([FromServices]IMyService myService)
{
    return "Ok";
}

属性注入

ASP.NET Core内置的依赖注入是不支持属性注入的。但是Autofac支持,用法如下:

老规矩,先定义服务和实现

public interface IUserService
{
    string Get();
}

public class UserService : IUserService
{
    public string Get()
    {
        return "User";
    }
}

然后注册服务

  • 默认情况下,控制器的构造函数参数由DI容器来管理吗,而控制器实例本身却是由ASP.NET Core框架来管理,所以这样“属性注入”是无法生效的
  • 通过AddControllersAsServices方法,将控制器交给 autofac 容器来处理,这样就可以使“属性注入”生效了
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddControllersAsServices();
}

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterModule<AutofacModule>();
}

public class AutofacModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<UserService>().As<IUserService>();

        var controllerTypes = Assembly.GetExecutingAssembly().GetExportedTypes()
            .Where(type => typeof(ControllerBase).IsAssignableFrom(type))
            .ToArray();

        // 配置所有控制器均支持属性注入
        builder.RegisterTypes(controllerTypes).PropertiesAutowired();
    }
}

最后,我们在控制器中通过属性来接收服务实例

public class ValuesController : ControllerBase
{
    public IUserService UserService { get; set; }

    [HttpGet]
    public string Get()
    {
        return UserService.Get();
    }
}

通过调用Get接口,我们就可以得到IUserService的实例,从而得到响应

User

一些注意事项

  • 避免使用服务定位模式。尽量避免使用GetService来获取服务实例,而应该使用DI。
using Microsoft.Extensions.DependencyInjection;

public class ValuesController : ControllerBase
{
    private readonly IServiceProvider _serviceProvider;

    // 应通过依赖注入的方式获取服务实例
    public ValuesController(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    [HttpGet]
    public string Get()
    {
        // 尽量避免通过 GetService 方法获取服务实例
        var myService = _serviceProvider.GetService<IMyService>();

        return "Ok";
    }
}
  • 避免在ConfigureServices中调用BuildServiceProvider。因为这会导致创建第二个DI容器的副本,从而导致注册的单例服务出现多个副本。
public void ConfigureServices(IServiceCollection services)
{
    // 不要在该方法中调用该方法
    var serviceProvider = services.BuildServiceProvider();
}
  • 一定要注意服务解析范围,不要在 Singleton 中解析 Transient 或 Scoped 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例)。允许的方式有:

1.在 Scoped 或 Transient 服务中解析 Singleton 服务

2.在 Scoped 或 Transient 服务中解析 Scoped 服务(不能和前面的Scoped服务相同)

  • 当在Development环境中运行、并通过 CreateDefaultBuilder生成主机时,默认的服务提供程序会进行如下检查:

1.不能在根服务提供程序解析 Scoped 服务,这会导致 Scoped 服务的生命周期提升为 Singleton,因为根容器在应用关闭时才会释放。

2.不能将 Scoped 服务注入到 Singleton 服务中

  • 随着业务增长,需要依赖注入的服务也越来越多,建议使用扩展方法,封装服务注入,命名为Add{Group_Name},如将所有 AppService 的服务注册封装起来
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ApplicationServiceCollectionExtensions
    {
        public static IServiceCollection AddApplicationService(this IServiceCollection services)
        {
            services.AddTransient<Service1>();
            services.AddScoped<Service2>();
            services.AddSingleton<Service3>();
            services.AddSingleton(sp => new Service4());

            return services;
        }
    }
}

然后在ConfigureServices中调用即可

public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationService();
}

框架默认提供的服务

以下列出一些常用的框架已经默认注册的服务:

服务类型 生命周期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transient
IHostApplicationLifetime Singleton
IHostLifetime Singleton
IWebHostEnvironment Singleton
IHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transient
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transient
Microsoft.Extensions.Logging.ILogger Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions Transient
Microsoft.Extensions.Options.IOptions Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

到此这篇关于理解ASP.NET Core 依赖注入(Dependency Injection)的文章就介绍到这了,更多相关ASP.NET Core 依赖注入内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解ASP.NET Core 在 JSON 文件中配置依赖注入

    前言 在上一篇文章中写了如何在MVC中配置全局路由前缀,今天给大家介绍一下如何在在 json 文件中配置依赖注入. 在以前的 ASP.NET 4+ (MVC,Web Api,Owin,SingalR等)时候,都是提供了专有的接口以供使用第三方的依赖注入组件,比如我们常用的会使用 Autofac.Untiy.String.Net 等,这些第三放依赖注入组件基本上都提供了一套配置注入或者配置生命周期的方式,除了直接配置到类里面之外,还提供了要么使用 xml 文件,要么使用 json 等,那么在新的

  • 详解ASP.NET Core 中的框架级依赖注入

    1.ASP.NET Core 中的依赖注入 此示例展示了框架级依赖注入如何在 ASP.NET Core 中工作. 其简单但功能强大,足以完成大部分的依赖注入工作.框架级依赖注入支持以下 scope: Singleton - 总是返回相同的实例 Transient - 每次都返回新的实例 Scoped - 在当前(request)范围内返回相同的实例 假设我们有两个要通过依赖注入来进行工作的工件: PageContext - 自定义请求上下文 Settings - 全局应用程序设置 这两个都是非常

  • ASP.NET Core依赖注入系列教程之控制反转(IoC)

    前言 ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化",我们将这些标准化的组件称为服务,ASP.NET在内部专门维护了一个DI容器来提供所需的服务.要了解这个DI容器以及现实其中的服务提供机制,我们先得知道什么是DI(Dependence Injection),而一旦我们提到DI,又不得不说IoC(Inverse of Control). 一.流程控

  • 详解asp.net core 依赖注入

    前言 好久没有写微博了,因为前段时间由于家庭原因决定从工作了3年多的北京转移到上海去.依赖注入在学习net core的时候也有写过类似的东西,只是实践的较少,结果来到上海新公司系统框架涉及到了这块知识点,所以在了解完自己的项目之后决定做一些相关的总结.接下来就让我们先来了解hewi依赖注入. 什么是依赖注入 依赖注入,全称是"依赖注入到容器", 容器(IOC容器)是一个设计模式,它也是个对象,你把某个类(不管有多少依赖关系)放入这个容器中,可以"解析"出这个类的实例

  • ASP.NET Core实现自动依赖注入

    在开发.NET Core web服务的时候,我们习惯使用自带的依赖注入容器来进行注入. 于是就会经常进行一个很频繁的的重复动作:定义一个接口->写实现类->注入 有时候会忘了写Add这一步,看到屏幕上的报错一脸懵逼,然后瞬间反应过来忘了注入了.赶紧补上serviceCollection.AddXXX这句话 虽然说有很多开源框架已经实现了类似的工作,比如AutoFac,Unity等依赖注入框架.但是这些库都太庞大了,我个人还是喜欢轻量级的实现. 定义一个枚举 [AttributeUsage(At

  • 理解ASP.NET Core 依赖注入(Dependency Injection)

    目录 依赖注入 什么是依赖注入 依赖注入有什么好处 ASP.NET Core内置的依赖注入 服务生存周期 服务释放 TryAdd{Lifetime}扩展方法 解析同一服务的多个不同实现 Replace && Remove 扩展方法 Autofac 服务解析和注入 构造函数注入 方法注入 属性注入 一些注意事项 框架默认提供的服务 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可. 微软官方文档-DI 依赖注入有什么好处 依赖

  • ASP.NET Core依赖注入详解

    目录 一.什么是依赖注入 二.使用框架提供的服务 三.注册服务 四.生命周期 五.请求服务 六.设计你的依赖服务 ASP.NET Core的底层设计支持和使用依赖注入.ASP.NET Core应用程序可以利用内置的框架服务将它们注入到启动类的方法中,并且应用程序服务能够配置注入.由ASP.NET Core提供的默认服务容器提供了最小功能集,并不是要取代其它容器. 一.什么是依赖注入 依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术.将类

  • ASP.NET Core依赖注入(DI)讲解

    ASP.NET Core的底层设计支持和使用依赖注入.ASP.NET Core 应用程序可以利用内置的框架服务将服务注入到启动类的方法中,并且应用程序服务也可以配置注入.由ASP.NET Core 提供的默认服务容器提供了最小功能集,并不是取代其他容器. 1.浅谈依赖注入 依赖注入(Dependency injection,DI)是一种实现对象和依赖者之间松耦合的技术,将类用来执行其操作的这些对象以注入的方式提供给该类,而不是直接实例化依赖项或者使用静态引用.一般情况,类会通过构造函数声明器2依

  • ASP.NET Core  依赖注入框架的使用

    目录 一.IoC框架 二.IoC-Autofac 三..NET Core中自带DI的使用 四.Autofac 使用 五.批量注入 前言: 还记得上篇文章中ASP.NET Core 依赖注入详细最后提及到,假如服务越来越多怎么处理呢,本篇文章将会带来解决办法.这篇是接上一篇文章的,概念方面的可以参考上一篇文章. 一.IoC框架 先说说常见的Ioc框架吧. Autofac: 目前net用的比较多,好多大佬的项目比较优先选择的框架. Ninject: 已经很少用了,还时在很早的文章中见过. Unity

  • ASP.NET Core 依赖注入详细

    目录 一.控制反转 二.好莱坞法则 三.流程控制 四.三种依赖注入方式 1.构造器注入 2.属性注入 3.方法注入 五.生命周期 六.ASP.Net Core 中自带的注入 前言: ASP.NET Core 应用在启动过程中会依赖各种组件提供服务,而这些组件会以接口的形式标准化,这些组件这就是我们所说的服务,ASP.NET Core框架建立在一个底层的依赖注入框架之上,它使用容器提供所需的服务.要了解依赖注入容器以及它的机制,我们需要了解什么是依赖注入. 一.控制反转 说道依赖注入就不得不提控制

  • ASP.NET Core依赖注入系列教程之服务的注册与提供

    前言 在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core中的DI容器体现为一个实现了IServiceProvider接口的对象. ServiceProvider与ServiceDescriptor 服务的注册与提供     利用ServiceProvider来提供服务     提供一个服务实例的集合     获取ServiceProvider自身对

  • ASP.NET Core依赖关系注入

    1.前言 面向对象设计(OOD)里有一个重要的思想就是依赖倒置原则(DIP),并由该原则牵引出依赖注入(DI).控制反转(IOC)及其容器等老生常谈的概念,初学者很容易被这些概念搞晕(包括我在内),在学习Core依赖注入服务之前,下面让我们先了解下依赖倒置原则(DIP).依赖注入(DI).控制反转(IOC)等概念,然后再深入学习Core依赖注入服务. 2.依赖倒置原则(DIP) 高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口.通俗来讲,就是高层模块定义接口,低层模块负责实现.

  • .Net Core依赖注入IOC和DI介绍

    名词解释 说起依赖注入,很多人会想起两个词:IOC和DI. IOC(Inversion of Control)=控制反转 DI(Dependency Injection)=依赖注入 IOC其实并不是某项具体的实现技术,它是一种思路(或者是设计理念),这一点很多有经验的同学都会搞混.如果用白话来讲,IOC就意味着把对象交给IOC容器控制,而不是在对象内部控制.简而言之就是“容器控制对象”. 套入到程序来说,我们有两个class,A.B,如果B要用到A的方法,传统的方法是在B中添加A的引用(比如ne

随机推荐