1. 程式人生 > >【轉載】ASP.NET Core 依賴註入

【轉載】ASP.NET Core 依賴註入

microsoft 銷毀 分享圖片 ros inview 我們 col info 服務

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

為什麽要寫這個博客

DI在.NET Core裏面被提到了一個非常重要的位置, 這篇文章主要再給大家普及一下關於依賴註入的概念,身邊有工作六七年的同事還個東西搞不清楚。另外再介紹一下.NET Core的DI實現以及對實例生命周期的管理(這個是經常面試會問到的問題)。最後再給大家簡單介紹一下在控制臺以及Mvc下如何使用DI,以及如何把默認的Service Container 替換成Autofac。

一、什麽是依賴註入(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()
}

大師說,這樣不好。你不應該自己創建它,而是應該由你的調用者給你。於是你通過構造函數讓外界把這兩個依傳給你。

private ILoginService<ApplicationUser> _loginService;
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;

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

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

    public Guid OperationId => _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($"signletone2: {singletone2.OperationId}");
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($"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。

三、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>>();

四、如何替換其它的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。

【轉載】ASP.NET Core 依賴註入