依賴注入


依賴注入

原文:Dependency Injection
作者:Steve Smith
翻譯:劉浩楊
校對:許登洋(Seay)高嵩

ASP.NET Core 的底層設計支持和使用依賴注入。ASP.NET Core 應用程序可以利用內置的框架服務將它們注入到啟動類的方法中,並且應用程序服務能夠配置注入。由 ASP.NET Core 提供的默認服務容器提供了最小功能集並且不是要取代其他容器。

查看或下載示例代碼

 

什么是依賴注入

依賴注入(Dependency injection,DI)是一種實現對象及其合作者或依賴項之間松散耦合的技術。將類用來執行其操作(Action)的這些對象以某種方式提供給該類,而不是直接實例化合作者或使用靜態引用。通常,類會通過它們的構造函數聲明其依賴關系,允許它們遵循 顯示依賴原則 (Explicit Dependencies Principle) 。這種方法被稱為 “構造函數注入(constructor injection)”。

當類的設計使用 DI 思想,它們耦合更加松散,因為它們沒有對它們的合作者直接硬編碼的依賴。這遵循 依賴倒置原則(Dependency Inversion Principle),其中指出 “高層模塊不應該依賴於低層模塊;兩者都應該依賴於抽象。” 類要求在它們構造時向其提供抽象(通常是 interfaces ),而不是引用特定的實現。提取接口的依賴關系和提供這些接口的實現作為參數也是 策略設計模式(Strategy design pattern) 的一個示例。

當系統被設計使用 DI ,很多類通過它們的構造函數(或屬性)請求其依賴關系,有一個類被用來創建這些類及其相關的依賴關系是很有幫助的。這些類被稱為 容器(containers) ,或者更具體地,控制反轉(Inversion of Control,IoC) 容器或者依賴注入(Dependency injection,DI)容器。容器本質上是一個工廠,負責提供向它請求的類型實例。如果一個給定類型聲明它具有依賴關系,並且容器已經被配置為提供依賴類型,它將把創建依賴關系作為創建請求實例的一部分。通過這種方式,可以向類型提供復雜的依賴關系而不需要任何硬編碼的類型構造。除了創建對象的依賴關系,容器通常還會管理應用程序中對象的生命周期。

ASP.NET Core 包含了一個默認支持構造函數注入的簡單內置容器(由 IServiceProvider 接口表示),並且 ASP.NET 使某些服務可以通過 DI 獲取。ASP.NET 的容器指的是它管理的類型為 services。在這篇文章的其余部分, services 是指由 ASP.NET Core 的 IoC 容器管理的類型。你在應用程序 Startup 類的 ConfigureServices 方法中配置內置容器的服務。

注解
Martin Fowler 寫過一篇全面的文章發表在 Inversion of Control Containers and the Dependency Injection Pattern. Microsoft 模式與實踐小組(Microsoft Patterns and Practices)也有豐富的關於 Dependency Injection 的描述。

注解
本文介紹了依賴注入,因為它適用於所有的 ASP.NET 應用程序。 MVC 控制器中的依賴注入包含在 Dependency Injection and Controllers

 

使用框架提供的服務

Startup 類的 ConfigureServices 方法負責定義應用程序將使用的服務,包括平台功能,比如 Entity Framework Core 和 ASP.NET Core MVC 。最初, IServiceCollection 只向 ConfigureServices 提供了幾個服務定義。下面是一個如何使用一些擴展方法(如 AddDbContextAddIdentity 和 AddMvc )向容器中添加額外服務的例子。

復制代碼
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.AddMvc(); // Add application services. services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>(); }
 

ASP.NET 提供的功能和中間件,例如 MVC,遵循約定——使用一個單一的 AddService擴展方法來注冊所有該功能所需的服務。

小技巧
你可以在 Startup 的方法中通過它們的參數列表請求一些框架提供的服務 - 查看 Application Startup 獲取更多信息。

當然,除了使用各種框架功能配置應用程序,你也能夠使用 ConfigureServices 配置你自己的應用程序服務。

 

注冊你自己的服務

你可以按照如下方式注冊你自己的應用程序服務。第一個泛型類型表示將要從容器中請求的類型(通常是一個接口)。第二個泛型類型表示將由容器實例化並且用於完成這些請求的具體類型。

復制代碼
services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>();
 

注解
每個 services.Add<service> 調用添加(和可能配置)服務。 例如: services.AddMvc() 添加 MVC 需要的服務。

AddTransient 方法用於將抽象類型映射到為每一個需要它的對象分別實例化的具體服務。這被稱作為服務的 生命周期(lifetime),另外的生命周期選項在下面描述。為你注冊的每一個服務選擇合適的生命周期是重要的。應該為每個請求的類提供一個新的服務實例?應該在一個給定的網絡請求中使用一個實例?或者應該在應用程序生命周期中使用單例?

在這篇文章的示例中,有一個名稱為 CharactersController 的簡單控制器。它的 Index 方法顯示已經存儲在應用程序中的當前字符列表,並且,如果它不存在的話,初始化具有少量字符的集合。值得注意的是,雖然應用程序使用 Entity Framework Core 和 ApplicationDbContext 類作為持久化,這在控制器中都不是顯而易見的。相反,具體的數據訪問機制被抽象在遵循 倉儲模式(repository pattern) 的 ICharacterRepository 接口后面。 ICharacterRepository 的實例是通過構造函數請求並分配給一個私有字段,然后用來訪問所需的字符。

復制代碼
public class CharactersController : Controller { private readonly ICharacterRepository _characterRepository; public CharactersController(ICharacterRepository characterRepository) { _characterRepository = characterRepository; } // GET: /characters/ public IActionResult Index() { PopulateCharactersIfNoneExist(); var characters = _characterRepository.ListAll(); return View(characters); } private void PopulateCharactersIfNoneExist() { if (!_characterRepository.ListAll().Any()) { _characterRepository.Add(new Character("Darth Maul")); _characterRepository.Add(new Character("Darth Vader")); _characterRepository.Add(new Character("Yoda")); _characterRepository.Add(new Character("Mace Windu")); } } }
 

ICharacterRepository 只定義了控制器需要使用 Character 實例的兩個方法。

復制代碼
using System.Collections.Generic; using DependencyInjectionSample.Models; namespace DependencyInjectionSample.Interfaces { public interface ICharacterRepository { IEnumerable<Character> ListAll(); void Add(Character character); } }
 

這個接口在運行時使用一個具體的 CharacterRepository 類型來實現。

注解
在 CharacterRepository 類中使用 DI 的方式是一個你可以在你的應用程序服務遵循的通用模型,不只是在“倉儲”或者數據訪問類中。

復制代碼
using System.Collections.Generic; using System.Linq; using DependencyInjectionSample.Interfaces; namespace DependencyInjectionSample.Models { public class CharacterRepository : ICharacterRepository { private readonly ApplicationDbContext _dbContext; public CharacterRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public IEnumerable<Character> ListAll() { return _dbContext.Characters.AsEnumerable(); } public void Add(Character character) { _dbContext.Characters.Add(character); _dbContext.SaveChanges(); } } }
 

注意的是 CharacterRepository 需要一個 ApplicationDbContext 在它的構造函數中。依賴注入用於像這樣的鏈式方法並不少見,每個請求依次請求它的依賴關系。容器負責解析所有的依賴關系,並返回完全解析后的服務。

注解
創建請求對象,和它需要的所有對象,以及那些需要的所有對象,有時稱為一個 對象圖(object graph)。同樣的,必須解析依賴關系的集合通常稱為 依賴樹(dependency tree) 或者 依賴圖(dependency graph)。

在這種情況下, ICharacterRepository 和 ApplicationDbContext 都必須在 Startup 類ConfigureServices 方法的服務容器中注冊。ApplicationDbContext 的配置調用 AddDbContext<T> 擴展方法。下面的代碼展示 CharacterRepository 類型的注冊。

復制代碼
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase() ); // Add framework services. services.AddMvc(); // Register application services. services.AddScoped<ICharacterRepository, CharacterRepository>(); services.AddTransient<IOperationTransient, Operation>(); services.AddScoped<IOperationScoped, Operation>(); services.AddSingleton<IOperationSingleton, Operation>(); services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty)); services.AddTransient<OperationService, OperationService>(); }
 

Entity Framework 上下文應當使用 Scoped 生命周期添加到服務容器中。如果你使用上圖所示的幫助方法則這是自動處理的。倉儲將使 Entity Framework 使用相同的生命周期。

警告
最主要的危險是要小心從單例解析一個 Scoped 服務。在這種情況下很可能處理后續請求的時候服務會出現不正確的狀態。

 

服務生命周期和注冊選項

ASP.NET 服務可以被配置為以下生命周期:

瞬時
瞬時(Transient)生命周期服務在它們每次請求時被創建。這一生命周期適合輕量級的,無狀態的服務。

作用域
作用域(Scoped)生命周期服務在每次請求被創建一次。

單例
單例(Singleton)生命周期服務在它們第一次被請求時創建(或者如果你在 ConfigureServices
運行時指定一個實例)並且每個后續請求將使用相同的實例。如果你的應用程序需要單例行為,建議讓服務容器管理服務的生命周期而不是在自己的類中實現單例模式和管理對象的生命周期。

服務可以用多種方式在容器中注冊。我們已經看到了如何通過指定具體類型用來注冊一個給定類型的服務實現。除此之外,可以指定一個工廠,它將被用來創建需要的實例。第三種方式是直接指定要使用的類型的實例,在這種情況下容器將永遠不會嘗試創建一個實例。

為了說明這些生命周期和注冊選項之間的差異,考慮一個簡單的接口將一個或多個任務表示為有一個唯一標識符 OperationId 的 操作 。依據我們如何配置這個服務的生命周期,容器將為請求的類提供相同或不同的服務實例。要弄清楚哪一個生命周期被請求,我們將創建每一個生命周期選項的類型:

復制代碼
using System; namespace DependencyInjectionSample.Interfaces { public interface IOperation { Guid OperationId { get; } } public interface IOperationTransient : IOperation { } public interface IOperationScoped : IOperation { } public interface IOperationSingleton : IOperation { } public interface IOperationSingletonInstance : IOperation { } }
 

我們使用 Operation 類實現這些接口。它的構造函數接收一個 Guid,若未提供則生成一個新的 Guid

接下來,在 ConfigureServices 中,每一個類型根據它們命名的生命周期被添加到容器中:

復制代碼
services.AddTransient<IOperationTransient, Operation>(); services.AddScoped<IOperationScoped, Operation>(); services.AddSingleton<IOperationSingleton, Operation>(); services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty)); services.AddTransient<OperationService, OperationService>();
 

請注意, IOperationSingletonInstance 服務使用一個具有已知 Guid.Empty ID 的具體實例,所以該類型在使用時是明確的。我們還注冊了一個依賴於其他每個 Operation 類型的 OperationService,因此在一個請求中對於每個操作類型,該服務獲取相同的實例或創建一個新的實例作為控制器將是明確的。所有服務通過屬性暴露依賴關系,因此它們可以顯示在視圖中。

復制代碼
using DependencyInjectionSample.Interfaces; namespace DependencyInjectionSample.Services { public class OperationService { public IOperationTransient TransientOperation { get; } public IOperationScoped ScopedOperation { get; } public IOperationSingleton SingletonOperation { get; } public IOperationSingletonInstance SingletonInstanceOperation { get; } public OperationService(IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance instanceOperation) { TransientOperation = transientOperation; ScopedOperation = scopedOperation; SingletonOperation = singletonOperation; SingletonInstanceOperation = instanceOperation; } } }
 

為了證明對象的生命周期在應用程序的每個單獨的請求內,還是請求之間,此示例包含OperationsController
 請求每一個 IOperation 類型和 OperationService。 Index action 接下來顯示所有控制器和服務的 OperationId 值。

復制代碼
using DependencyInjectionSample.Interfaces; using DependencyInjectionSample.Services; using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionSample.Controllers { public class OperationsController : Controller { private readonly OperationService _operationService; private readonly IOperationTransient _transientOperation; private readonly IOperationScoped _scopedOperation; private readonly IOperationSingleton _singletonOperation; private readonly IOperationSingletonInstance _singletonInstanceOperation; public OperationsController(OperationService operationService, IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance singletonInstanceOperation) { _operationService = operationService; _transientOperation = transientOperation; _scopedOperation = scopedOperation; _singletonOperation = singletonOperation; _singletonInstanceOperation = singletonInstanceOperation; } public IActionResult Index() { // viewbag contains controller-requested services ViewBag.Transient = _transientOperation; ViewBag.Scoped = _scopedOperation; ViewBag.Singleton = _singletonOperation; ViewBag.SingletonInstance = _singletonInstanceOperation; // operation service has its own requested services ViewBag.Service = _operationService; return View(); } } }
 

現在兩個獨立的請求到這個 controller action:
lifetimes_request1.png
lifetimes_request2.png

觀察 OperationId 值在請求和請求之間的變化。

  • 瞬時(Transient) 對象總是不同的;向每一個控制器和每一個服務提供了一個新的實例
  • 作用域(Scoped) 對象在一次請求中是相同的,但在不同請求中是不同的
  • 單例(Singleton) 對象對每個對象和每個請求是相同的(無論是否在 ConfigureServices 中提供實例)
 

請求服務

來自 HttpContext 的一次 ASP.NET 請求中可用的服務通過 RequestServices 集合公開的。
request-services.png

請求服務將你配置的服務和請求描述為應用程序的一部分。當你的對象指定依賴關系,這些滿足要求的對象通過查找 RequestServices 中對應的類型得到,而不是 ApplicationServices

通常,你不應該直接使用這些屬性,而更傾向於通過類的構造函數請求需要的類的類型,並且讓框架來注入依賴關系。這將會生成更易於測試的 (查看 Testing) 和更松散耦合的類。

注解
更傾向於請求依賴關系作為構造函數的參數來訪問 RequestServices 集合。

 

設計你的依賴注入服務

你應該設計你的依賴注入服務來獲取它們的合作者。這意味着在你的服務中避免使用有狀態的靜態方法調用(代碼被稱為 static cling)和直接實例化依賴的類型。當選擇實例化一個類型還是通過依賴注入請求它時,它可以幫助記住這句話, New is Glue。通過遵循 面向對象設計的 SOLID 原則,你的類將傾向於小、易於分解及易於測試。

如果你發現你的類往往會有太多的依賴關系被注入時該怎么辦?這通常表明你的類試圖做太多,並且可能違反了單一職責原則(SRP) - 單一職責原則。看看你是否可以通過轉移一些職責到一個新的類來重構類。請記住,你的 Controller 類應該重點關注用戶界面(User Interface,UI),因此業務規則和數據訪問實現細節應該保存在這些適合單獨關注的類中。

關於數據訪問,如果你已經在 Startup 類中配置了 EF,那么你能夠方便的注入 Entity Framework 的 DbContext 類型到你的控制器中。然而,最好不要在你的 UI 項目直接依賴 DbContext。相反,依賴於一個抽象(比如一個倉儲接口),並且限定使用 EF (或其他任何數據訪問技術)來實現這個接口。這將減少應用程序和特定的數據訪問策略之間的耦合,並且使你的應用程序代碼更容易測試。

 

替換默認的服務容器

內置的服務容器的意圖在於提供框架的基本需求並且大多數客戶應用程序建立在它之上。然而,開發人員可以很容易地使用他們的首選容器替換默認容器。ConfigureServices 方法通常返回 void,但是如果改變它的簽名返回 IServiceProvider,可以配置並返回一個不同的容器。有很多 IOC 容器可用於 .NET。在這個例子中, Autofac 包被使用。

首先,在 project.json 的 dependencies 屬性中添加適當的容器包:

復制代碼
"dependencies" : { "Autofac": "4.0.0-rc2-237", "Autofac.Extensions.DependencyInjection": "4.0.0-rc2-200" },
 

接着,在 ConfigureServices 中配置容器並返回 IServiceProvider

復制代碼
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 container.Resolve<IServiceProvider>(); }
 

注解
當使用第三方 DI 容器時,你必須更改 ConfigureServices 讓它返回 IServiceProvider 而不是 void

最后,在 DefaultModule 中配置 Autofac:

復制代碼
public class DefaultModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<CharacterRepository>().As<ICharacterRepository>(); } }
 

在運行時,Autofac 將被用來解析類型和注入依賴關系。 了解更多有關使用 Autofac 和 ASP.NET Core

 

建議

當使用依賴注入時,請記住以下建議:

  • DI 針對具有復雜依賴關系的對象。控制器,服務,適配器和倉儲都是可能被添加到 DI 的對象的例子。
    避免直接在 DI 中存儲數據和配置。例如,用戶的購物車通常不應該被添加到服務容器中。配置應該使用 Options Model。 同樣, 避免 “數據持有者” 對象只是為了允許訪問其他對象而存在。如果可能的話,最好是通過 DI 獲取實際的項。
  • 避免靜態訪問服務。
  • 避免在應用程序代碼中服務定位。
  • 避免靜態訪問 HttpContext 。

注解
像所有的建議,你可能遇到必須忽視其中一個的情況。我們發現了少見的例外 – 非常特別的情況是框架本身。

記住,依賴注入是靜態/全局對象訪問模式的 另一選擇。如果你把它和靜態對象訪問混合的話,你將無法了解 DI 的有用之處。

 

附加資源

由於水平有限,錯漏之處在所難免,歡迎大家批評指正,不勝感激,我們將及時修正。
dotNet Core Studying Group:436035237
 
分類:  ASP.NET CORE


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM