任务21 :了解ASP.NET Core 依赖注入,看这篇就够了

DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚。另外再介绍一下.NET  Core的DI实现以及对实例生命周期的管理(这个是经常面试会问到的问题)。最后再给大家简单介绍一下在控制台以及Mvc下如何使用DI,以及如何把默认的Service Container 替换成Autofac。
  • 一、什么是依赖注入

  • 1.1 依赖
  • 1.2 什么注入
  • 为什么反转
  • 何为容器
  • 二、.NET Core DI

  • 2.1 实例的注册
  • 2.2 实例生命周期之单例
  • 2.3 实例生命周期之Tranisent
  • 2.4 实例生命周期之Scoped
  • 三、DI在ASP.NET Core中的应用

  • 3.1 在Startup类中初始化
  • 3.2 Controller中使用
  • 3.3 View中使用
  • 3.4 通过HttpContext来获取
  • 四、如何替换其它的Ioc容器

一、什么是依赖注入(Denpendency Injection)

这也是个老身常谈的问题,到底依赖注入是什么? 为什么要用它? 初学者特别容易对控制反转IOC(Iversion of Control),DI等概念搞晕。

1.1依赖

当一个类需要另一个类协作来完成工作的时候就产生了依。比如我们在AccountController这个控制器需要完成和用户相关的注册、登录 等事情。其中的登录我们由EF结合Idnetity来完成,所以我们封装了一个EFLoginService。这里AccountController就有一个ILoginService的依
这里有一个设计原则:依于抽象,而不是具体的实现。所以我们给EFLoginService定义了一个接口,抽象了LoginService的行为。

1.2 什么是注入

注入体现的是一个IOC(控制反转的的思想)。在反转之前 ,我们先看看正转。
AccountController自己来实例化需要的依
private ILoginService<ApplicationUser> _loginService;
public AccountController()
{
  _loginService = new EFLoginService()
}

大师说,这样不好。你不应该自己创建它,而是应该由你的调用者给你。于是你通过构造函数让外界把这两个依传给你。

 public 
 AccountController(ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

把依赖的创建丢给其它人,自己只负责使用,其它人丢给你依赖的这个过程理解为注入。

1.3 为什么要反转?

为了在业务变化的时候尽少改动代码可能造成的问题。
比如我们现在要把从EF中去验证登录改为从Redis去读,于是我们加了一个 RedisLoginService。这个时候我们只需要在原来注入的地方改一下就可以了。
 var controller = new AccountController(new EFLoginService());
controller.Login(userName, password);

// 用Redis来替换原来的EF登录

var controller = new AccountController(new RedisLoginService());
controller.Login(userName, password);

1.4 何为容器

上面我们在使用AccountController的时候,我们自己通过代码创建了一个ILoggingServce的实例。想象一下,一个系统中如果有100个这样的地方,我们是不是要在100个地方做这样的事情? 控制是反转了,依的创建也移交到了外部。现在的问题是依太多,我们需要一个地方统一管理系统中所有的依,容器诞生了。
容器负责两件事情:
  • 绑定服务与实例之间的关系
  • 获取实例,并对实例进行管理(创建与销毁)

二、.NET Core DI

2.1 实例的注册

前面讲清楚DI和Ioc的关键概念之后,我们先来看看在控制台中对.NET Core DI的应用。在.NET Core中DI的核心分为两个组件:IServiceCollection和 IServiceProvider。
  • IServiceCollection 负责注册
  • IServiceProvider 负责提供实例
通过默认的 ServiceCollection(在Microsoft.Extensions.DependencyInjection命名空间下)有三个方法:
var serviceCollection = new ServiceCollection()
  .AddTransient<ILoginService, EFLoginService>()
  .AddSingleton<ILoginService, EFLoginService>()
  .AddScoped<ILoginService, EFLoginService>();
这三个方法都是将我们的实例注册进去,只不过实例的生命周期不一样。什么时候生命周期我们下一节接着讲。
ServiceCollection的默认实现是提供一个ServiceDescriptor的List
public interface IServiceCollection : IList<ServiceDescriptor>
{
}

我们上面的AddTransient、AddSignletone和Scoped方法是IServiceCollection的扩展方法, 都是往这个List里面添加ServiceDescriptor。

private static IServiceCollection Add(
  IServiceCollection collection,
  Type serviceType,
  Type implementationType,
  ServiceLifetime lifetime)
{
  var descriptor = 
  new ServiceDescriptor(serviceType, implementationType, lifetime);
  collection.Add(descriptor);
  return collection;
}

2.2 实例的生命周期之单例

我们上面看到了,.NET Core DI 为我们提供的实例生命周其包括三种:
  • Transient: 每一次GetService都会创建一个新的实例
  • Scoped:  在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)
  • Singleton :整个应用程序生命周期以内只创建一个实例 
对应了Microsoft.Extensions.DependencyInjection.ServiceLifetime的三个枚举值
public enum ServiceLifetime
{
  Singleton,
  Scoped,
  Transient
}
为了大家能够更好的理解这个生命周期的概念我们做一个测试:
定义一个最基本的IOperation里面有一个 OperationId的属性,IOperationSingleton也是一样,只不过是另外一个接口。
public interface IOperation
{
        Guid OperationId { get; }
}
public interface IOperationSingleton : IOperation { }
public interface IOperationTransient : IOperation{}
public interface IOperationScoped : IOperation{}

我们的 Operation实现很简单,可以在构造函数中传入一个Guid进行赋值,如果没有的话则自已New一个 Guid。

public class Operation : 
  IOperationSingleton,
  IOperationTransient,
  IOperationScoped
{
    private Guid _guid;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Operation</span><span class="hljs-params">()</span> </span>{
    _guid = Guid.NewGuid();
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Operation</span><span class="hljs-params">(Guid guid)</span>
</span>{
    _guid = guid;
}

<span class="hljs-keyword">public</span> Guid OperationId =&gt; _guid;

}

在程序内我们可以多次调用ServiceProvider的GetService方法,获取到的都是同一个实例。

var services = new ServiceCollection();
// 默认构造
services.AddSingleton<IOperationSingleton, Operation>();
// 自定义传入Guid空值
services.AddSingleton<IOperationSingleton>(
  new Operation(Guid.Empty));
// 自定义传入一个New的Guid
services.AddSingleton <IOperationSingleton>(
  new Operation(Guid.NewGuid()));

var provider = services.BuildServiceProvider();

// 输出singletone1的Guid
var singletone1 = provider.GetService<IOperationSingleton>();
Console.WriteLine($"signletone1: {singletone1.OperationId}");

// 输出singletone2的Guid
var singletone2 = provider.GetService<IOperationSingleton>();
Console.WriteLine((<span class="hljs-string">"signletone2: {singletone2.OperationId}"</span>); Console.WriteLine()"singletone1 == singletone2 ? : { singletone1 == singletone2 }");

我们对IOperationSingleton注册了三次,最后获取两次,大家要注意到我们获取到的始终都是我们最后一次注册的那个给了一个Guid的实例,前面的会被覆盖。

2.3 实例生命周期之Tranisent 

这次我们获取到的IOperationTransient为两个不同的实例。

var services = new ServiceCollection();
services.AddTransient<IOperationTransient, Operation>();

var provider = services.BuildServiceProvider();

var transient1 = provider.GetService<IOperationTransient>();
Console.WriteLine($"transient1: {transient1.OperationId}");

var transient2 = provider.GetService<IOperationTransient>();
Console.WriteLine((<span class="hljs-string">"transient2: {transient2.OperationId}"</span>); Console.WriteLine()"transient1 == transient2 ? :
{ transient1 == transient2 }"
);

2.4 实例生命周期之Scoped

.NET Core人IServiceProvider提供CreateScope产生一个新的ServiceProvider范围,在这个范围下的Scope标注的实例将只会是同一个实例。换句话来说:用Scope注册的对象,在同一个ServiceProvider的 Scope下相当于单例。
同样我们先分别注册IOperationScoped、IOperationTransient和IOperationSingletone 这三个实例,用对应的Scoped、Transient、和Singleton生命周期。
 var services = new ServiceCollection()
.AddScoped<IOperationScoped, Operation>()
.AddTransient<IOperationTransient, Operation>()
.AddSingleton<IOperationSingleton, Operation>();

接下来我们用ServiceProvider.CreateScope方法创建一个Scope

var provider = services.BuildServiceProvider();
using (var scope1 = provider.CreateScope())
{
    var p = scope1.ServiceProvider;
<span class="hljs-keyword">var</span> scopeobj1 = p.GetService&lt;IOperationScoped&gt;();
<span class="hljs-keyword">var</span> transient1 = p.GetService&lt;IOperationTransient&gt;();
<span class="hljs-keyword">var</span> singleton1 = p.GetService&lt;IOperationSingleton&gt;();

<span class="hljs-keyword">var</span> scopeobj2 = p.GetService&lt;IOperationScoped&gt;();
<span class="hljs-keyword">var</span> transient2 = p.GetService&lt;IOperationTransient&gt;();
<span class="hljs-keyword">var</span> singleton2 = p.GetService&lt;IOperationSingleton&gt;();

Console.WriteLine(
    $<span class="hljs-string">"scope1: { scopeobj1.OperationId },"</span> +
    $<span class="hljs-string">"transient1: {transient1.OperationId}, "</span> +
    $<span class="hljs-string">"singleton1: {singleton1.OperationId}"</span>);

Console.WriteLine($<span class="hljs-string">"scope2: { scopeobj2.OperationId }, "</span> +
    $<span class="hljs-string">"transient2: {transient2.OperationId}, "</span> +
    $<span class="hljs-string">"singleton2: {singleton2.OperationId}"</span>);

}

接下来

scope1: 200d1e63-d024-4cd3-88c9-35fdf5c00956, 
transient1: fb35f570-713e-43fc-854c-972eed2fae52, 
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225

scope2: 200d1e63-d024-4cd3-88c9-35fdf5c00956,
transient2: 2766a1ee-766f-4116-8a48-3e569de54259,
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225

如果再创建一个新的Scope运行,

scope1: 29f127a7-baf5-4ab0-b264-fcced11d0729, 
transient1: 035d8bfc-c516-44a7-94a5-3720bd39ce57, 
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225

scope2: 29f127a7-baf5-4ab0-b264-fcced11d0729,
transient2: 74c37151-6497-4223-b558-a4ffc1897d57,
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225

大家注意到上面我们一共得到了 4个Transient实例,2个Scope实例,1个Singleton实例。
这有什么用?
如果在Mvc中用过Autofac的InstancePerRequest的同学就知道,有一些对象在一个请求跨越多个Action或者多个Service、Repository的时候,比如最常用的DBContext它可以是一个实例。即能减少实例初始化的消耗,还能实现跨Service事务的功能。(注:在ASP.NET Core中所有用到EF的Service 都需要注册成Scoped )
 
而实现这种功能的方法就是在整个reqeust请求的生命周期以内共用了一个Scope。

三、DI在ASP.NET Core中的应用

3.1在Startup类中初始化

ASP.NET Core可以在Startup.cs的  ConfigureService中配置DI,大家看到 IServiceCollection这个参数应该就比较熟悉了。
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ILoginService<ApplicationUser>, 
      EFLoginService>();
    services.AddMvc();
)

ASP.NET Core的一些组件已经提供了一些实例的绑定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的扩展方法。

public static IMvcBuilder AddMvc(this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }
<span class="hljs-keyword">var</span> builder = services.AddMvcCore();

builder.AddApiExplorer();
builder.AddAuthorization();
AddDefaultFrameworkParts(builder.PartManager);
...

}

3.2 Controller中使用

一般可以通过构造函数或者属性来实现注入,但是官方推荐是通过构造函数。这也是所谓的显式依
 private ILoginService<ApplicationUser> _loginService;
public AccountController(
  ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

我们只要在控制器的构造函数里面写了这个参数,ServiceProvider就会帮我们注入进来。这一步是在Mvc初始化控制器的时候完成的,我们后面再介绍到Mvc的时候会往细里讲。

3.3 View中使用

在View中需要用@inject 再声明一下,起一个别名。
@using MilkStone.Services;
@model MilkStone.Models.AccountViewModel.LoginViewModel
@inject ILoginService<ApplicationUser>  loginService
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head></head>
<body>
  @loginService.GetUserName()
</body>
</html>

3.4 通过 HttpContext来获取实例

HttpContext下有一个RequestedService同样可以用来获取实例对象,不过这种方法一般不推荐。同时要注意GetService<>这是个范型方法,默认如果没有添加Microsoft.Extension.DependencyInjection的using,是不用调用这个方法的。
HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();

四、如何替换其它的Ioc容器

Autofac也是不错的选择,但我们首先要搞清楚为什么要替换掉默认的 DI容器?,替换之后有什么影响?.NET Core默认的实现对于一些小型的项目完全够用,甚至大型项目麻烦点也能用,但是会有些麻烦,原因在于只提供了最基本的AddXXXX方法来绑定实例关系,需要一个一个的添加。如果项目可能要添加好几百行这样的方法。
如果熟悉Autofac的同学可能会这下面这样的代码有映象。
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>));

builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));

这会给我们的初始化带来一些便利性,我们来看看如何替换Autofac到ASP.NET Core。我们只需要把Startup类里面的 ConfigureService的 返回值从 void改为 IServiceProvider即可。而返回的则是一个AutoServiceProvider。

public IServiceProvider ConfigureServices(
  IServiceCollection services){
    services.AddMvc();
    // Add other framework services
<span class="hljs-comment">// Add Autofac</span>
<span class="hljs-keyword">var</span> containerBuilder = <span class="hljs-keyword">new</span> ContainerBuilder();
containerBuilder.RegisterModule&lt;DefaultModule&gt;();
containerBuilder.Populate(services);
<span class="hljs-keyword">var</span> container = containerBuilder.Build();
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> AutofacServiceProvider(container);

}

4.1 有何变化 

其中很大的一个变化在于,Autofac 原来的一个生命周期InstancePerRequest,将不再有效。正如我们前面所说的,整个request的生命周期被ASP.NET Core管理了,所以Autofac的这个将不再有效。我们可以使用 InstancePerLifetimeScope ,同样是有用的,对应了我们ASP.NET Core DI 里面的Scoped。 
本文首发于公众号jessetalk,如需转载请保留二维码,谢谢。
	<div class="section section-blog-info">
		<div class="row">
			<div class="col-md-6">
				<div class="entry-categories">分类:						<span class="label label-primary"><a href="http://www.jessetalk.cn/category/tech/">技术随笔</a></span>					</div>
				<div class="entry-tags">标签:<span class="entry-tag"><a href="http://www.jessetalk.cn/tag/asp-net-core/" rel="tag">asp.net core</a></span><span class="entry-tag"><a href="http://www.jessetalk.cn/tag/di/" rel="tag">DI</a></span></div>				</div>
			
    <div class="col-md-6">
        <div class="entry-social">
            <a target="_blank" rel="tooltip" data-original-title="分享到 Facebook" class="btn btn-just-icon btn-round btn-facebook" href="https://www.facebook.com/sharer.php?u=http://www.jessetalk.cn/2017/11/06/di-in-aspnetcore/">
               <i class="fa fa-facebook"></i>
            </a>
            
            <a target="_blank" rel="tooltip" data-original-title="分享至微博" class="btn btn-just-icon btn-round btn-twitter" href="http://twitter.com/share?url=http://www.jessetalk.cn/2017/11/06/di-in-aspnetcore/&amp;text=%E4%BA%86%E8%A7%A3ASP.NET%20Core%20%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%EF%BC%8C%E7%9C%8B%E8%BF%99%E7%AF%87%E5%B0%B1%E5%A4%9F%E4%BA%86">
               <i class="fa fa-twitter"></i>
            </a>
            
            <a rel="tooltip" data-original-title=" Share on Email" class="btn btn-just-icon btn-round" href="mailto:?subject=了解ASP.NET%20Core%20依赖注入,看这篇就够了&amp;body=http://www.jessetalk.cn/2017/11/06/di-in-aspnetcore/">
               <i class="fa fa-envelope"></i>
           </a>
        </div>
	</div>			</div>
		<hr>

15 条评论

dp · 2017年11月25日 下午12:54

好文!!!

w3i · 2017年12月11日 下午5:48

用心!

52dotnet · 2017年12月13日 下午10:13

总算治好了我对依赖注入的多年不理解,收藏了!

尼古拉屎找死 · 2017年12月17日 上午10:47

写的不错,继续加油!!

陈家涛 · 2017年12月20日 上午9:54

看不懂,太抽象了

长空 · 2017年12月22日 下午11:01

文章是极好的,讲的还是很透彻的。
从其他地方看过来的,网站做的非常好啊!正是我很喜欢的极简风格。。我自己的网站用的模版,有点搓的样子。
MD文档的渲染的样式可以调整下,不如你在博客上的显示效果。。

Hei幕。 · 2018年1月9日 下午8:56

看完了 会点基本的

hdgao · 2018年1月27日 下午4:08

能否将这节的实际演示的例子给我发一份到我的邮箱,想好好参考学习一下,不错写的支持

潇洒走一回 · 2018年2月7日 下午4:48

好文,看了那么多依赖注入思想的文章,你这里讲解得最简易,最易懂。对于NET Core DI的这个,还得继续看看。

xiaocui · 2018年4月11日 下午6:01

讲的通俗易懂,很不错。

低级码农 · 2018年5月9日 下午10:44

感谢博主,解决了我好多疑问。

华仔 · 2018年5月25日 下午4:59

身边有工作六七年的同事还个东西搞不清楚。 这句话有别字

鸟窝 · 2018年5月27日 下午6:03

如何实现,在asp.net core中不使用autofac,而实现属性注入??我们都知道.net core中默认注入方式是构造函数注入,但是总感觉构造函数注入,没有属性注入方便,求解!

低级码农 · 2018年6月12日 下午11:33

如果熟悉Autofac的同学可能会这下面这样的代码有“映象”。 有错别字哦

niunan · 2018年7月3日 下午6:51

支持fcrf

发表评论

电子邮件地址不会被公开。 必填项已用*标注

【推广】 免费学中医,健康全家人
原文地址:https://www.cnblogs.com/owenzh/p/11306872.html