ASP.NET?Core依賴注入詳解
ASP.NET Core的底層設(shè)計(jì)支持和使用依賴注入。ASP.NET Core應(yīng)用程序可以利用內(nèi)置的框架服務(wù)將它們注入到啟動(dòng)類的方法中,并且應(yīng)用程序服務(wù)能夠配置注入。由ASP.NET Core提供的默認(rèn)服務(wù)容器提供了最小功能集,并不是要取代其它容器。
一、什么是依賴注入
依賴注入(Dependency injection,DI)是一種實(shí)現(xiàn)對(duì)象及其合作者或依賴項(xiàng)之間松散耦合的技術(shù)。將類用來(lái)執(zhí)行其操作的這些對(duì)象以某種方式提供給該類,而不是直接實(shí)例化合作者或使用靜態(tài)引用。通常,類會(huì)通過(guò)它們的構(gòu)造函數(shù)聲明其依賴關(guān)系,允許它們遵循顯示依賴原則。這種方法被稱為“構(gòu)造函數(shù)注入”。
當(dāng)類的設(shè)計(jì)使用DI思想時(shí),它們的耦合更加松散,因?yàn)樗鼈儧](méi)有對(duì)它們的合作者直接硬編碼的依賴。這遵循“依賴倒置原則(Dependency Inversion Principle)”,其中指出,“高層模塊不應(yīng)該依賴于低層模塊;兩者都應(yīng)該依賴于抽象”。類要求在它們構(gòu)造時(shí)向其提供抽象(通常是interfaces),而不是引用特定的實(shí)現(xiàn)。提取接口的依賴關(guān)系和提供這些接口的實(shí)現(xiàn)作為參數(shù)也是“策略設(shè)計(jì)模式”的一個(gè)示例。
當(dāng)系統(tǒng)被設(shè)計(jì)使用DI,很多類通過(guò)它們的構(gòu)造函數(shù)(或?qū)傩裕┱?qǐng)求其依賴關(guān)系,當(dāng)一個(gè)類被用來(lái)創(chuàng)建這些類及其相關(guān)的依賴關(guān)系是很有幫助的。這些類被稱為“容器(containers)”,或者更具體地被稱為“控制反轉(zhuǎn)(Inversion of Control,IOC)容器”或者“依賴注入(Dependency injection,DI)容器”。容器本質(zhì)上是一個(gè)工廠,負(fù)責(zé)提供向它請(qǐng)求的類型實(shí)例。如果一個(gè)給定類型聲明它具有依賴關(guān)系,并且容器已經(jīng)被配置為提供依賴類型,那么它將把創(chuàng)建依賴關(guān)系作為創(chuàng)建請(qǐng)求實(shí)例的一部分。通過(guò)這種方式,可以向類型提供復(fù)雜的依賴關(guān)系而不需要任何硬編碼的類型構(gòu)造。除了創(chuàng)建對(duì)象的依賴關(guān)系外,容器通常還會(huì)管理應(yīng)用程序中對(duì)象的生命周期。
ASP.NET Core包含了一個(gè)默認(rèn)支持構(gòu)造函數(shù)注入的簡(jiǎn)單內(nèi)置容器(由IServiceProvider接口表示),并且ASP.NET Core使某些服務(wù)可以通過(guò)DI獲取。ASP.NET Core的容器指的是它管理的類型為services。services是指由ASP.NET Core的IOC容器管理的類型。我們可以在應(yīng)用程序Startup類的ConfigureServices方法中配置內(nèi)置容器的服務(wù)。
二、使用框架提供的服務(wù)
Startup類中的ConfigureServices方法負(fù)責(zé)定義應(yīng)用程序?qū)⑹褂玫姆?wù),包括平臺(tái)功能,比如EntityFramework Core和ASP.NET Core MVC。最初,IServiceCollection只向ConfigureServices提供了幾個(gè)服務(wù)定義。如下面的例子:
除了使用默認(rèn)提供的幾個(gè)服務(wù)定義,我們還可以自己添加。下面是一個(gè)如何使用一些擴(kuò)展方法(如AddDbContext,AddIdentity)向容器中添加額外服務(wù)的例子:
public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服務(wù)
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加MVC服務(wù)
services.AddControllersWithViews();
}ASP.NET提供的功能和中間件,例如MVC,遵循約定使用一個(gè)單一的AddService擴(kuò)展方法來(lái)注冊(cè)所有該功能所需的服務(wù)。
當(dāng)然,除了使用各種框架功能配置應(yīng)用程序外,還可以使用ConfigureServices來(lái)配置自己的應(yīng)用程序服務(wù)。
三、注冊(cè)服務(wù)
可以按照下面的方式注冊(cè)自己的應(yīng)用程序服務(wù)。第一個(gè)泛型類型表示將要從容器中請(qǐng)求的類型(這里的類型通常是一個(gè)接口)。第二個(gè)泛型類型表示將由容器實(shí)例化并且用于完成這些請(qǐng)求的具體類型:
// 添加自己的服務(wù) // IRepository是一個(gè)接口,表示要請(qǐng)求的類型 // UserRepository表示IRepository接口的具體實(shí)現(xiàn)類型 services.AddTransient<IRepository, UserRepository>();
每個(gè)services.Add<service>調(diào)用添加服務(wù)。例如,services.AddControllersWithViews()表示添加MVC需要的服務(wù)。
在示例中,有一個(gè)名稱為CharactersController的控制器。它的Index方法顯示已經(jīng)存儲(chǔ)在應(yīng)用程序的當(dāng)前字符列表,并且,如果它不存在的話,則初始化具有少量字符的集合。值得注意的是:雖然應(yīng)用程序使用Entity Framework Core和AppDbContext類作為持久化工具,這在控制器中都不是顯而易見的。相反,具體的數(shù)據(jù)訪問(wèn)機(jī)制被抽象在遵循倉(cāng)儲(chǔ)模式的ICharacterRepository接口后面。ICharacterRepository實(shí)例是通過(guò)構(gòu)造函數(shù)注入的,并且分配給一個(gè)私有字段,然后用來(lái)訪問(wèn)所需的字符:
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>
/// 通過(guò)構(gòu)造函數(shù)注入并且給私有字段賦值
/// </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實(shí)例的兩個(gè)方法:
using DependencyInjectionDemo.Model;
using System.Collections.Generic;
namespace DependencyInjectionDemo.Repository
{
public interface ICharacterRepository
{
IEnumerable<Character> ListAll();
int Add(Character character);
}
}這個(gè)接口在運(yùn)行時(shí)需要使用一個(gè)具體的CharacterRepository類型來(lái)實(shí)現(xiàn)。
在CharacterRepository類中使用DI的方式是一個(gè)可以在你的應(yīng)用程序服務(wù)遵循的通用模型,不只是在“倉(cāng)儲(chǔ)”或者數(shù)據(jù)訪問(wèn)類中:
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>
/// 通過(guò)構(gòu)造函數(shù)注入,并且給私有字段賦值
/// </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需要一個(gè)AppDbContext在它的構(gòu)造函數(shù)中。依賴注入用于像這樣的鏈?zhǔn)椒椒ú⒉簧僖姡總€(gè)請(qǐng)求依次請(qǐng)求它的依賴關(guān)系。容器負(fù)責(zé)解析所有的依賴關(guān)系,并返回完全解析后的服務(wù)。
創(chuàng)建請(qǐng)求對(duì)象和它需要的所有對(duì)象,以及那些需要的所有對(duì)象,有時(shí)稱為一個(gè)對(duì)象圖。同樣的,必須解析依賴關(guān)系的集合通常稱為依賴樹或者依賴圖。
在這種情況下,ICharacterRepository和AppDbContext都必須在Startup類的ConfigureServices方法的服務(wù)容器中注冊(cè)。AppDbContext配置調(diào)用AddDbContex<T>擴(kuò)展方法。下面的代碼展示了ICharacterRepository和AppDbContext類型的注冊(cè):
public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服務(wù)
// 這里是注冊(cè)AppDbContext使用AddDbContext<T>的形式
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加自己的服務(wù)
// IRepository是一個(gè)接口,表示要請(qǐng)求的類型
// UserRepository表示IRepository接口的具體實(shí)現(xiàn)類型
services.AddTransient<IRepository, UserRepository>();
// 注冊(cè)ICharacterRepository類型
services.AddTransient<ICharacterRepository, CharacterRepository>();
// 添加MVC服務(wù)
services.AddControllersWithViews();
}Entity Framework Core的數(shù)據(jù)上下文應(yīng)當(dāng)使用Scope的生命周期添加到服務(wù)容器中。如果使用上面的AddDbContext<T>方法則會(huì)自動(dòng)處理。倉(cāng)儲(chǔ)將使用與Entity Framework Core相同的生命周期。
四、生命周期
ASP.NET Core服務(wù)可以配置為以下三種生命周期:
- Transient:瞬時(shí)生命周期。瞬時(shí)生命周期服務(wù)在它們每次請(qǐng)求時(shí)被創(chuàng)建。這一生命周期適合輕量級(jí)的、無(wú)狀態(tài)的服務(wù)。
- Scoped:作用域生命周期。作用域生命周期服務(wù)在每次請(qǐng)求時(shí)被創(chuàng)建一次。
- Singleton:?jiǎn)卫芷凇卫芷诜?wù)在它們第一次被請(qǐng)求時(shí)創(chuàng)建,并且每個(gè)后續(xù)請(qǐng)求將使用相同的實(shí)例。如果你的應(yīng)用程序需要單例行為,則建議讓服務(wù)容器管理服務(wù)的生命周期,而不是在自己的類中實(shí)現(xiàn)單例模式和管理對(duì)象的生命周期。
服務(wù)可以用多種方式在容器中注冊(cè)。我們已經(jīng)看到了如何通過(guò)指定具體類型來(lái)注冊(cè)一個(gè)給定類型的服務(wù)實(shí)現(xiàn)。除此之外,可以指定一個(gè)工廠,它將被用來(lái)創(chuàng)建需要的實(shí)例。第三種方式是直接指定要使用的類型的實(shí)例。在這種情況下,容器將永遠(yuǎn)不會(huì)嘗試創(chuàng)建一個(gè)實(shí)例。
為了說(shuō)明這些生命周期和注冊(cè)選項(xiàng)之間的差異,考慮一個(gè)簡(jiǎn)單的接口將一個(gè)或多個(gè)任務(wù)表示為有一個(gè)唯一標(biāo)識(shí)符OperationId的操作。根據(jù)我們配置這個(gè)服務(wù)的生命周期的方法,容器將為請(qǐng)求的類提供相同或不同的服務(wù)實(shí)例。為了弄清楚哪一個(gè)生命周期被請(qǐng)求,我們需要?jiǎng)?chuàng)建每一個(gè)生命周期選項(xiàng)的類型。我們先定義一個(gè)接口,里面定義基接口和三種注入模式的接口:
using System;
namespace DependencyInjectionDemo.Repository
{
/// <summary>
/// 基接口
/// </summary>
public interface IOperationRepository
{
Guid GetOperationId();
}
/// <summary>
/// 瞬時(shí)接口
/// </summary>
public interface IOperationTransientRepository: IOperationRepository
{
}
/// <summary>
/// 作用域接口
/// </summary>
public interface IOperationScopeRepository : IOperationRepository
{
}
/// <summary>
/// 單例接口
/// </summary>
public interface IOperationSingletonRepository : IOperationRepository
{
}
}我們使用OperationRepository類來(lái)實(shí)現(xiàn)這些接口:
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中,每一個(gè)類型根據(jù)它們命名的生命周期被添加到容器中:
public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服務(wù)
// 這里是注冊(cè)AppDbContext使用AddDbContext<T>的形式
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加自己的服務(wù)
// IRepository是一個(gè)接口,表示要請(qǐng)求的類型
// UserRepository表示IRepository接口的具體實(shí)現(xiàn)類型
services.AddTransient<IRepository, UserRepository>();
// 注冊(cè)ICharacterRepository類型
services.AddTransient<ICharacterRepository, CharacterRepository>();
// 添加瞬時(shí)生命周期
services.AddTransient<IOperationTransientRepository, OperationTransientRepository>();
// 添加作用域生命周期
services.AddScoped<IOperationScopeRepository, OperationScopeRepository>();
// 添加單例生命周期
services.AddSingleton<IOperationSingletonRepository, OperationSingletonRepository>();
// 添加MVC服務(wù)
services.AddControllersWithViews();
}然后添加一個(gè)控制器:
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>
/// 通過(guò)構(gòu)造函數(shù)實(shí)現(xiàn)注入
/// </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();
}
}
}對(duì)應(yīng)的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>然后我們打開兩個(gè)瀏覽器,刷新多次,只會(huì)發(fā)現(xiàn)“TransientGuid” 和“ScopedGuid”的值在不斷變化,而“SingletonGuid”的值是不會(huì)變化的,這就體現(xiàn)了單例模式的作用,如下圖所示:

但是這樣還不夠,要知道我們的Scoped的解讀是“生命周期橫貫整次請(qǐng)求”,但是現(xiàn)在演示起來(lái)和Transient好像沒(méi)有什么區(qū)別(因?yàn)閮蓚€(gè)頁(yè)面每次瀏覽器請(qǐng)求仍然是獨(dú)立的,并不包含于一次中),所以我們采用以下代碼來(lái)演示下(同一請(qǐng)求源):
@*引入命名空間*@
@using DependencyInjectionDemo.Repository
@*通過(guò)該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>在運(yùn)行程序執(zhí)行:

可以看到:每次請(qǐng)求的時(shí)候Scope生命周期在同一請(qǐng)求中是不變的,而Transient生命周期還是會(huì)不斷變化的。
- 瞬時(shí)(Transient):對(duì)象總是不同的,向每一個(gè)控制器和每一個(gè)服務(wù)提供了一個(gè)新的實(shí)例(同一個(gè)頁(yè)面內(nèi)的Transient也是不同的)。
- 作用域(Scoped):對(duì)象在一次請(qǐng)求中是相同的,但在不同請(qǐng)求中是不同的(在同一個(gè)頁(yè)面內(nèi)多個(gè)Scoped是相同的,在不同頁(yè)面中是不同的)。
- 單例(Singleton):對(duì)象對(duì)每個(gè)對(duì)象和每個(gè)請(qǐng)求是相同的(無(wú)論是否在ConfigureServices中提供實(shí)例)。
五、請(qǐng)求服務(wù)
來(lái)自HttpContext的一次ASP.NET請(qǐng)求中,可用的服務(wù)是通過(guò)RequestServices集合公開的。
請(qǐng)求服務(wù)將你配置的服務(wù)和請(qǐng)求描述為應(yīng)用程序的一部分。在你的對(duì)象指定依賴關(guān)系后,這些滿足要求的對(duì)象可通過(guò)查找RequestServices中對(duì)應(yīng)的類型得到,而不是ApplicationServices。
通過(guò),不應(yīng)該直接使用這些屬性,而是通過(guò)類的構(gòu)造函數(shù)請(qǐng)求需要的類的類型,并且讓框架來(lái)注入依賴關(guān)系。這將會(huì)生成更易于測(cè)試的和更松散耦合的類。
六、設(shè)計(jì)你的依賴服務(wù)
應(yīng)該設(shè)計(jì)你的依賴注入服務(wù)來(lái)獲取它們的合作者。這意味著在你的服務(wù)中,避免使用有狀態(tài)的靜態(tài)方法調(diào)用和直接實(shí)例化依賴的類型。
如果你的類有太多的依賴關(guān)系被注入時(shí)該怎么辦?這通常表明你的類試圖做太多,并且可能違反了單一職責(zé)原則。看看是否可以通過(guò)轉(zhuǎn)移一些職責(zé)到一個(gè)新的類來(lái)重構(gòu)。
注意,你的Controller類應(yīng)該重點(diǎn)關(guān)注用戶界面(UI),因此業(yè)務(wù)規(guī)則和數(shù)據(jù)訪問(wèn)實(shí)現(xiàn)細(xì)節(jié)應(yīng)該保存在這些適合單獨(dú)關(guān)注的類中。
關(guān)于數(shù)據(jù)訪問(wèn),如果你已經(jīng)在Startup類中配置了EF,那么你能夠方便地注入Entity Framework的DBContext類型到你的控制器中。然而,最好不要在你的UI項(xiàng)目中直接依賴DBContext。相反,應(yīng)該依賴于一個(gè)抽象(比如一個(gè)倉(cāng)儲(chǔ)接口),并且限定使用EF(或其他任何數(shù)據(jù)訪問(wèn)技術(shù))來(lái)實(shí)現(xiàn)這個(gè)接口。這將減少應(yīng)用程序和特定的數(shù)據(jù)訪問(wèn)策略之間的耦合,并且使你的應(yīng)用程序代碼更容易測(cè)試。
GitHub示例代碼:https://github.com/jxl1024/DependencyInjection
到此這篇關(guān)于ASP.NET Core依賴注入的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ASP.NETWeb服務(wù)器驗(yàn)證控件如何使用
這篇文章主要介紹了ASP.NETWeb服務(wù)器驗(yàn)證控件如何使用,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2015-09-09
ASP.NET Core 2.0 使用支付寶PC網(wǎng)站支付實(shí)現(xiàn)代碼
這篇文章主要介紹了ASP.NET Core 2.0 使用支付寶PC網(wǎng)站支付實(shí)現(xiàn)代碼,需要的朋友可以參考下2017-10-10
WinForm中窗體間的數(shù)據(jù)傳遞交互的一些方法
通過(guò)子窗口向外引發(fā)一個(gè)事件,父窗口去實(shí)現(xiàn)該事件,我們可以再不關(guān)閉父窗口和子窗口的情況下進(jìn)行數(shù)據(jù)的傳輸顯示2012-12-12
高效.NET臟字過(guò)濾算法與應(yīng)用實(shí)例
這篇文章主要介紹了高效.NET臟字過(guò)濾算法與應(yīng)用方法,結(jié)合實(shí)例形式分析了.NET字符串過(guò)濾操作相關(guān)技巧,需要的朋友可以參考下2016-08-08
GridView中動(dòng)態(tài)設(shè)置CommandField是否可用或可見的小例子
GridView中動(dòng)態(tài)設(shè)置CommandField是否可用或可見的小例子,需要的朋友可以參考一下2013-05-05
WPF項(xiàng)目在設(shè)計(jì)界面調(diào)用后臺(tái)代碼
這篇文章介紹了WPF項(xiàng)目在設(shè)計(jì)界面調(diào)用后臺(tái)代碼的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04
asp.net 擴(kuò)展GridView 增加單選按鈕列的代碼
asp.net 擴(kuò)展GridView 增加單選按鈕列的代碼2010-02-02

