【转载】ASP.NET Core 依赖注入

本文转自:http://www.jessetalk.cn/2017/11/06/di-in-aspnetcore/html

为何要写这个博客

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

1、什么是依赖注入(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()
}

大师说,这样很差。你不该该本身建立它,而是应该由你的调用者给你。因而你经过构造函数让外界把这两个依传给你。dom

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

把依赖的建立丢给其它人,本身只负责使用,其它人丢给你依赖的这个过程理解为注入。ide

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个地方作这样的事情? 控制是反转了,依的建立也移交到了外部。如今的问题是依太多,咱们须要一个地方统一管理系统中全部的依,容器诞生了。
 
容器负责两件事情:
  • 绑定服务与实例之间的关系
  • 获取实例,并对实例进行管理(建立与销毁)

2、.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;

    public Operation() {
        _guid = Guid.NewGuid();
    }

    public Operation(Guid guid)
    {
        _guid = guid;
    }

    public Guid OperationId => _guid;
}

在程序内咱们能够屡次调用ServiceProvider的GetService方法,获取到的都是同一个实例。ui

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($"signletone2: {singletone2.OperationId}");
Console.WriteLine($"singletone1 == singletone2 ? : { singletone1 == singletone2 }");

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

 

2.3 实例生命周期之Tranisent 

此次咱们获取到的IOperationTransient为两个不一样的实例。spa

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($"transient2: {transient2.OperationId}");
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;
    var scopeobj1 = p.GetService<IOperationScoped>();
    var transient1 = p.GetService<IOperationTransient>();
    var singleton1 = p.GetService<IOperationSingleton>();

    var scopeobj2 = p.GetService<IOperationScoped>();
    var transient2 = p.GetService<IOperationTransient>();
    var singleton2 = p.GetService<IOperationSingleton>();

    Console.WriteLine($"scope1: { scopeobj1.OperationId },\ntransient1: {transient1.OperationId},\nsingleton1: {singleton1.OperationId}");

    Console.WriteLine($"scope2: { scopeobj2.OperationId },\ntransient2: {transient2.OperationId},\nsingleton2: {singleton2.OperationId}");
}

 接下来

 

若是再建立一个新的Scope运行,

你们注意到上面咱们一共获得了 4个Transient实例,2个Scope实例,1个Singleton实例。

这有什么用?
 
若是在Mvc中用过Autofac的InstancePerRequest的同窗就知道,有一些对象在一个请求跨越多个Action或者多个Service、Repository的时候,好比最经常使用的DBContext它能够是一个实例。即能减小实例初始化的消耗,还能实现跨Service事务的功能。(注:在ASP.NET Core中全部用到EF的Service 都须要注册成Scoped )
 
而实现这种功能的方法就是在整个reqeust请求的生命周期之内共用了一个Scope。

3、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));
    }

    var 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>>();

4、如何替换其它的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

    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder.RegisterModule<DefaultModule>();
    containerBuilder.Populate(services);
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
}

4.1 有何变化 

其中很大的一个变化在于,Autofac 原来的一个生命周期InstancePerRequest,将再也不有效。正如咱们前面所说的,整个request的生命周期被ASP.NET Core管理了,因此Autofac的这个将再也不有效。咱们可使用 InstancePerLifetimeScope ,一样是有用的,对应了咱们ASP.NET Core DI 里面的Scoped。 

相关文章
相关标签/搜索