ASP.NET Core的底層設計支援和使用依賴注入。ASP.NET Core應用程式可以利用内置的架構服務将它們注入到啟動類的方法中,并且應用程式服務能夠配置注入。由ASP.NET Core提供的預設服務容器提供了最小功能集,并不是要取代其它容器。
一、什麼是依賴注入
依賴注入(Dependency injection,DI)是一種實作對象及其合作者或依賴項之間松散耦合的技術。将類用來執行其操作的這些對象以某種方式提供給該類,而不是直接執行個體化合作者或使用靜态引用。通常,類會通過它們的構造函數聲明其依賴關系,允許它們遵循顯示依賴原則。這種方法被稱為“構造函數注入”。
當類的設計使用DI思想時,它們的耦合更加松散,因為它們沒有對它們的合作者直接寫死的依賴。這遵循“依賴倒置原則(Dependency Inversion Principle)”,其中指出,“高層子產品不應該依賴于低層子產品;兩者都應該依賴于抽象”。類要求在它們構造時向其提供抽象(通常是interfaces),而不是引用特定的實作。提取接口的依賴關系和提供這些接口的實作作為參數也是“政策設計模式”的一個示例。
當系統被設計使用DI,很多類通過它們的構造函數(或屬性)請求其依賴關系,當一個類被用來建立這些類及其相關的依賴關系是很有幫助的。這些類被稱為“容器(containers)”,或者更具體地被稱為“控制反轉(Inversion of Control,IOC)容器”或者“依賴注入(Dependency injection,DI)容器”。容器本質上是一個工廠,負責提供向它請求的類型執行個體。如果一個給定類型聲明它具有依賴關系,并且容器已經被配置為提供依賴類型,那麼它将把建立依賴關系作為建立請求執行個體的一部分。通過這種方式,可以向類型提供複雜的依賴關系而不需要任何寫死的類型構造。除了建立對象的依賴關系外,容器通常還會管理應用程式中對象的生命周期。
ASP.NET Core包含了一個預設支援構造函數注入的簡單内置容器(由IServiceProvider接口表示),并且ASP.NET Core使某些服務可以通過DI擷取。ASP.NET Core的容器指的是它管理的類型為services。services是指由ASP.NET Core的IOC容器管理的類型。我們可以在應用程式Startup類的ConfigureServices方法中配置内置容器的服務。
二、使用架構提供的服務
Startup類中的ConfigureServices方法負責定義應用程式将使用的服務,包括平台功能,比如EntityFramework Core和ASP.NET Core MVC。最初,IServiceCollection隻向ConfigureServices提供了幾個服務定義。如下面的例子:
除了使用預設提供的幾個服務定義,我們還可以自己添加。下面是一個如何使用一些擴充方法(如AddDbContext,AddIdentity)向容器中添加額外服務的例子:
public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服務
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加MVC服務
services.AddControllersWithViews();
}
ASP.NET提供的功能和中間件,例如MVC,遵循約定使用一個單一的AddService擴充方法來注冊所有該功能所需的服務。
當然,除了使用各種架構功能配置應用程式外,還可以使用ConfigureServices來配置自己的應用程式服務。
三、注冊服務
可以按照下面的方式注冊自己的應用程式服務。第一個泛型類型表示将要從容器中請求的類型(這裡的類型通常是一個接口)。第二個泛型類型表示将由容器執行個體化并且用于完成這些請求的具體類型:
// 添加自己的服務
// IRepository是一個接口,表示要請求的類型
// UserRepository表示IRepository接口的具體實作類型
services.AddTransient<IRepository, UserRepository>();
每個services.Add<service>調用添加服務。例如,services.AddControllersWithViews()表示添加MVC需要的服務。
在示例中,有一個名稱為CharactersController的控制器。它的Index方法顯示已經存儲在應用程式的目前字元清單,并且,如果它不存在的話,則初始化具有少量字元的集合。值得注意的是:雖然應用程式使用Entity Framework Core和AppDbContext類作為持久化工具,這在控制器中都不是顯而易見的。相反,具體的資料通路機制被抽象在遵循倉儲模式的ICharacterRepository接口後面。ICharacterRepository執行個體是通過構造函數注入的,并且配置設定給一個私有字段,然後用來通路所需的字元:
using System.Linq;
using DependencyInjectionDemo.Model;
using DependencyInjectionDemo.Repository;
using Microsoft.AspNetCore.Mvc;
namespace DependencyInjectionDemo.Controllers
{
public class CharactersController : Controller
{
// 定義私有的隻讀字段
private readonly ICharacterRepository _characterRepository;
/// <summary>
/// 通過構造函數注入并且給私有字段指派
/// </summary>
/// <param name="characterRepository"></param>
public CharactersController(ICharacterRepository characterRepository)
{
_characterRepository = characterRepository;
}
public IActionResult Index()
{
return View();
}
private void PopulateCharactersIfNoneExist()
{
// 如果不存在則添加
if(!_characterRepository.ListAll().Any())
{
_characterRepository.Add(new Character("Tom"));
_characterRepository.Add(new Character("Jack"));
_characterRepository.Add(new Character("Kevin"));
}
}
}
}
ICharacterRepository接口中隻定義了控制器需要使用的Character執行個體的兩個方法:
using DependencyInjectionDemo.Model;
using System.Collections.Generic;
namespace DependencyInjectionDemo.Repository
{
public interface ICharacterRepository
{
IEnumerable<Character> ListAll();
int Add(Character character);
}
}
這個接口在運作時需要使用一個具體的CharacterRepository類型來實作。
在CharacterRepository類中使用DI的方式是一個可以在你的應用程式服務遵循的通用模型,不隻是在“倉儲”或者資料通路類中:
using DependencyInjectionDemo.Context;
using DependencyInjectionDemo.Model;
using System.Collections.Generic;
using System.Linq;
namespace DependencyInjectionDemo.Repository
{
public class CharacterRepository : ICharacterRepository
{
// 定義私有字段
private readonly AppDbContext _dbContext;
/// <summary>
/// 通過構造函數注入,并且給私有字段指派
/// </summary>
/// <param name="dbContext"></param>
public CharacterRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public int Add(Character character)
{
// 添加
_dbContext.Characters.Add(character);
// 儲存
return _dbContext.SaveChanges();
}
public IEnumerable<Character> ListAll()
{
return _dbContext.Characters.AsEnumerable();
}
}
}
需要注意的是,CharacterRepository需要一個AppDbContext在它的構造函數中。依賴注入用于像這樣的鍊式方法并不少見,每個請求依次請求它的依賴關系。容器負責解析所有的依賴關系,并傳回完全解析後的服務。
建立請求對象和它需要的所有對象,以及那些需要的所有對象,有時稱為一個對象圖。同樣的,必須解析依賴關系的集合通常稱為依賴樹或者依賴圖。
在這種情況下,ICharacterRepository和AppDbContext都必須在Startup類的ConfigureServices方法的服務容器中注冊。AppDbContext配置調用AddDbContex<T>擴充方法。下面的代碼展示了ICharacterRepository和AppDbContext類型的注冊:
public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服務
// 這裡是注冊AppDbContext使用AddDbContext<T>的形式
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加自己的服務
// IRepository是一個接口,表示要請求的類型
// UserRepository表示IRepository接口的具體實作類型
services.AddTransient<IRepository, UserRepository>();
// 注冊ICharacterRepository類型
services.AddTransient<ICharacterRepository, CharacterRepository>();
// 添加MVC服務
services.AddControllersWithViews();
}
Entity Framework Core的資料上下文應當使用Scope的生命周期添加到服務容器中。如果使用上面的AddDbContext<T>方法則會自動處理。倉儲将使用與Entity Framework Core相同的生命周期。
四、生命周期
ASP.NET Core服務可以配置為以下三種生命周期:
- Transient:瞬時生命周期。瞬時生命周期服務在它們每次請求時被建立。這一生命周期适合輕量級的、無狀态的服務。
- Scoped:作用域生命周期。作用域生命周期服務在每次請求時被建立一次。
- Singleton:單例生命周期。單例生命周期服務在它們第一次被請求時建立,并且每個後續請求将使用相同的執行個體。如果你的應用程式需要單例行為,則建議讓服務容器管理服務的生命周期,而不是在自己的類中實作單例模式和管理對象的生命周期。
服務可以用多種方式在容器中注冊。我們已經看到了如何通過指定具體類型來注冊一個給定類型的服務實作。除此之外,可以指定一個工廠,它将被用來建立需要的執行個體。第三種方式是直接指定要使用的類型的執行個體。在這種情況下,容器将永遠不會嘗試建立一個執行個體。
為了說明這些生命周期和注冊選項之間的差異,考慮一個簡單的接口将一個或多個任務表示為有一個唯一辨別符OperationId的操作。根據我們配置這個服務的生命周期的方法,容器将為請求的類提供相同或不同的服務執行個體。為了弄清楚哪一個生命周期被請求,我們需要建立每一個生命周期選項的類型。我們先定義一個接口,裡面定義基接口和三種注入模式的接口:
using System;
namespace DependencyInjectionDemo.Repository
{
/// <summary>
/// 基接口
/// </summary>
public interface IOperationRepository
{
Guid GetOperationId();
}
/// <summary>
/// 瞬時接口
/// </summary>
public interface IOperationTransientRepository: IOperationRepository
{
}
/// <summary>
/// 作用域接口
/// </summary>
public interface IOperationScopeRepository : IOperationRepository
{
}
/// <summary>
/// 單例接口
/// </summary>
public interface IOperationSingletonRepository : IOperationRepository
{
}
}
我們使用OperationRepository類來實作這些接口:
using System;
namespace DependencyInjectionDemo.Repository
{
public class OperationRepository : IOperationRepository
{
private readonly Guid _guid;
public OperationRepository()
{
_guid = Guid.NewGuid();
}
public Guid GetOperationId()
{
return _guid;
}
}
public class OperationTransientRepository : OperationRepository, IOperationTransientRepository
{
}
public class OperationScopeRepository : OperationRepository, IOperationScopeRepository
{
}
public class OperationSingletonRepository : OperationRepository, IOperationSingletonRepository
{
}
}
然後在Startup類的ConfigureServices中,每一個類型根據它們命名的生命周期被添加到容器中:
public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服務
// 這裡是注冊AppDbContext使用AddDbContext<T>的形式
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加自己的服務
// IRepository是一個接口,表示要請求的類型
// UserRepository表示IRepository接口的具體實作類型
services.AddTransient<IRepository, UserRepository>();
// 注冊ICharacterRepository類型
services.AddTransient<ICharacterRepository, CharacterRepository>();
// 添加瞬時生命周期
services.AddTransient<IOperationTransientRepository, OperationTransientRepository>();
// 添加作用域生命周期
services.AddScoped<IOperationScopeRepository, OperationScopeRepository>();
// 添加單例生命周期
services.AddSingleton<IOperationSingletonRepository, OperationSingletonRepository>();
// 添加MVC服務
services.AddControllersWithViews();
}
然後添加一個控制器:
using DependencyInjectionDemo.Repository;
using Microsoft.AspNetCore.Mvc;
namespace DependencyInjectionDemo.Controllers
{
public class OperationController : Controller
{
// 定義私有字段
private readonly IOperationTransientRepository _transientRepository;
private readonly IOperationScopeRepository _scopeRepository;
private readonly IOperationSingletonRepository _singletonRepository;
/// <summary>
/// 通過構造函數實作注入
/// </summary>
/// <param name="transientRepository"></param>
/// <param name="scopeRepository"></param>
/// <param name="singletonRepository"></param>
public OperationController(IOperationTransientRepository transientRepository,
IOperationScopeRepository scopeRepository,
IOperationSingletonRepository singletonRepository)
{
_transientRepository = transientRepository;
_scopeRepository = scopeRepository;
_singletonRepository = singletonRepository;
}
public IActionResult Index()
{
// ViewBag指派
ViewBag.TransientGuid = _transientRepository.GetOperationId();
ViewBag.ScopedGuid = _scopeRepository.GetOperationId();
ViewBag.SingletonGuid = _singletonRepository.GetOperationId();
return View();
}
}
}
對應的Index視圖代碼:
<div class="row">
<div>
<h2>GuidItem Shows</h2>
<h3>TransientGuid: @ViewBag.TransientGuid</h3>
<h3>ScopedGuid: @ViewBag.ScopedGuid</h3>
<h3>SingletonGuid: @ViewBag.SingletonGuid</h3>
</div>
</div>
然後我們打開兩個浏覽器,重新整理多次,隻會發現“TransientGuid” 和“ScopedGuid”的值在不斷變化,而“SingletonGuid”的值是不會變化的,這就展現了單例模式的作用,如下圖所示:

但是這樣還不夠,要知道我們的Scoped的解讀是“生命周期橫貫整次請求”,但是現在示範起來和Transient好像沒有什麼差別(因為兩個頁面每次浏覽器請求仍然是獨立的,并不包含于一次中),是以我們采用以下代碼來示範下(同一請求源):
@*引入命名空間*@
@using DependencyInjectionDemo.Repository
@*通過該inject引入*@
@inject IOperationTransientRepository OperationTransientRepository
@inject IOperationScopeRepository OperationScopeRepository
@inject IOperationSingletonRepository OperationSingletonRepository
<div class="row">
<div>
<h2>GuidItem Shows</h2>
<h3>TransientGuid: @OperationTransientRepository.GetOperationId()</h3>
<h3>ScopedGuid: @OperationScopeRepository.GetOperationId()</h3>
<h3>SingletonGuid: @OperationSingletonRepository.GetOperationId()</h3>
</div>
</div>
然後修改Index視圖:
<div class="row">
<div>
@Html.Partial("GuidPartial")
<h2>**************************</h2>
<h2>GuidItem Shows</h2>
<h3>TransientGuid: @ViewBag.TransientGuid</h3>
<h3>ScopedGuid: @ViewBag.ScopedGuid</h3>
<h3>SingletonGuid: @ViewBag.SingletonGuid</h3>
</div>
</div>
在運作程式執行:
可以看到:每次請求的時候Scope生命周期在同一請求中是不變的,而Transient生命周期還是會不斷變化的。
- 瞬時(Transient):對象總是不同的,向每一個控制器和每一個服務提供了一個新的執行個體(同一個頁面内的Transient也是不同的)。
- 作用域(Scoped):對象在一次請求中是相同的,但在不同請求中是不同的(在同一個頁面内多個Scoped是相同的,在不同頁面中是不同的)。
- 單例(Singleton):對象對每個對象和每個請求是相同的(無論是否在ConfigureServices中提供執行個體)。
五、請求服務
來自HttpContext的一次ASP.NET請求中,可用的服務是通過RequestServices集合公開的。
請求服務将你配置的服務和請求描述為應用程式的一部分。在你的對象指定依賴關系後,這些滿足要求的對象可通過查找RequestServices中對應的類型得到,而不是ApplicationServices。
通過,不應該直接使用這些屬性,而是通過類的構造函數請求需要的類的類型,并且讓架構來注入依賴關系。這将會生成更易于測試的和更松散耦合的類。
六、設計你的依賴服務
應該設計你的依賴注入服務來擷取它們的合作者。這意味着在你的服務中,避免使用有狀态的靜态方法調用和直接執行個體化依賴的類型。
如果你的類有太多的依賴關系被注入時該怎麼辦?這通常表明你的類試圖做太多,并且可能違反了單一職責原則。看看是否可以通過轉移一些職責到一個新的類來重構。
注意,你的Controller類應該重點關注使用者界面(UI),是以業務規則和資料通路實作細節應該儲存在這些适合單獨關注的類中。
關于資料通路,如果你已經在Startup類中配置了EF,那麼你能夠友善地注入Entity Framework的DBContext類型到你的控制器中。然而,最好不要在你的UI項目中直接依賴DBContext。相反,應該依賴于一個抽象(比如一個倉儲接口),并且限定使用EF(或其他任何資料通路技術)來實作這個接口。這将減少應用程式和特定的資料通路政策之間的耦合,并且使你的應用程式代碼更容易測試。
GitHub示例代碼:[email protected]:jxl1024/DependencyInjection.git