注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

依赖注入

什么是依赖注入

简单说,就是将对象的创建和销毁工作交给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
  1. 替换服务提供器工厂
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
// 通过此处将默认服务提供器工厂替换为 autofac
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
  1. 在 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 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例)。允许的方式有:

    • 在 Scoped 或 Transient 服务中解析 Singleton 服务
    • 在 Scoped 或 Transient 服务中解析 Scoped 服务(不能和前面的Scoped服务相同)
  • 当在Development环境中运行、并通过 CreateDefaultBuilder生成主机时,默认的服务提供程序会进行如下检查:

    • 不能在根服务提供程序解析 Scoped 服务,这会导致 Scoped 服务的生命周期提升为 Singleton,因为根容器在应用关闭时才会释放。
    • 不能将 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 - [03] Dependency Injection的更多相关文章

  1. ASP.NET Core 的 Dependency Injection

    ASP.NET Core使用了大量的DI(Dependency Injection)设计,有用过Autofac或类似的DI Framework对此应该不陌生.本篇将介绍ASP.NET Core的依赖注 ...

  2. 目录-理解ASP.NET Core

    <理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...

  3. 理解ASP.NET Core - [04] Host

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 本文会涉及部分 Host 相关的源码,并会附上 github 源码地址,不过为了降低篇幅,我会 ...

  4. 理解依赖注入(DI - Dependency Injection)

    系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of Contro ...

  5. 理解 ASP.NET Core: 处理管道

    理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...

  6. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  7. 理解ASP.NET Core - [02] Middleware

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 中间件 先借用微软官方文档的一张图: 可以看到,中间件实际上是一种配置在HTTP请求管道中,用 ...

  8. 理解ASP.NET Core - 配置(Configuration)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...

  9. 理解ASP.NET Core - 选项(Options)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...

随机推荐

  1. jvm源码解读--05 常量池 常量项的解析JVM_CONSTANT_Utf8

    当index=18的时候JVM_CONSTANT_Utf8 case JVM_CONSTANT_Utf8 : { cfs->guarantee_more(2, CHECK); // utf8_l ...

  2. 题解 P6892 [ICPC2014 WF]Baggage

    解题思路 非常好的一道构造题. 在手动模拟几个样例(也许不止几个)之后呢. 就可以发现其实这些操作的开始以及最后几步是有相通之处的. 关于手动模拟的样例放在了文章末尾,需要的自取. 先考虑操作次数. ...

  3. 用Python预测双色球福利彩票中奖号码(请不要当真)

    前言 双色球是中国福利彩票的一种玩法. 红球一共6组,每组从1-33中抽取一个,六个互相不重复.然后蓝球是从1-16中抽取一个数字,这整个组成的双色球 python从零基础入门到实战 今天,我们就用P ...

  4. BUUCTF-[HCTF 2018]admin(Unicode欺骗&伪造session)

    目录 方法一:Unicode欺骗 方法二:伪造session 参考文章 记一道flask下session伪造的题. 方法一:Unicode欺骗 拿到题目f12提示you are not admin,显 ...

  5. C++员工管理系统(封装+多态+继承+分类化+函数调用+读写文件+指针+升序降序算法等一系列知识结合)

    1 C++职工管理系统 2 该项目实现 八个 功能 3 1-增加功能 2-显示功能 3-删除功能 4-修改功能 4 5-查找功能 6-排序功能 7-清空功能 8-退出功能 5 实现多个功能使用了多个C ...

  6. 一张图说明 iaas paas saas的区别

    图片来源:https://www.bilibili.com/video/BV1QJ411S7c4  P2 云服务的三种模式 1laaS(基础设施即服务) laas(Infrastructure as ...

  7. Shell-07-文本处理grep

    文本处理sed sed:流编辑器,过滤和替换文本 工作原理:sed命令将当前处理的行读入模式空间进行处理,处理完把结果输出,并且清空模式空间. 然后再将下一行读入模式空间进行处理输出,以此类推,直到最 ...

  8. vue3 自己做一个轻量级状态管理,带跟踪功能,知道是谁改的,还能定位代码。

    上一篇 https://www.cnblogs.com/jyk/p/14706005.html 介绍了一个自己做的轻量级的状态管理,好多网友说,状态最重要的是跟踪功能,不能跟踪算啥状态管理? 因为vu ...

  9. mysql悲观锁和乐观锁

    悲观锁 查出来的数据必须是根据索引查出来的,不然锁表. # 语法 select * from table where id = 1 for update; 乐观锁 使用一个标识 cas 比较后替换 如 ...

  10. python创建一个简单的服务

    python -m http.server 8000 --bind 0.0.0.0 8000为端口 0.0.0.0允许远程访问