ASP.NET Core 支持依賴(lài)關(guān)系注入 (DI) 軟件設(shè)計(jì)模式,這是一種在類(lèi)及其依賴(lài)關(guān)系之間實(shí)現(xiàn)控制反轉(zhuǎn) (IoC) 的技術(shù)。
有關(guān)特定于 MVC 控制器中依賴(lài)關(guān)系注入的詳細(xì)信息,請(qǐng)參閱 在 ASP.NET Core 中將依賴(lài)項(xiàng)注入到控制器。
依賴(lài)項(xiàng)是另一個(gè)對(duì)象所需的任何對(duì)象。 使用應(yīng)用中其他類(lèi)所依賴(lài)的 WriteMessage 方法檢查以下 MyDependency 類(lèi):
C#
public class MyDependency
{
public MyDependency()
{
}
public Task WriteMessage(string message)
{
Console.WriteLine(
$"MyDependency.WriteMessage called. Message: {message}");
return Task.FromResult(0);
}
}
可以創(chuàng)建 MyDependency 類(lèi)的實(shí)例以使 WriteMessage 方法可用于類(lèi)。 MyDependency 類(lèi)是 IndexModel 類(lèi)的依賴(lài)項(xiàng):
C#
public class IndexModel : PageModel
{
MyDependency _dependency = new MyDependency();
public async Task OnGetAsync()
{
await _dependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
該類(lèi)創(chuàng)建并直接依賴(lài)于 MyDependency 實(shí)例。 代碼依賴(lài)關(guān)系(如前面的示例)存在問(wèn)題,應(yīng)該避免使用,原因如下:
依賴(lài)關(guān)系注入通過(guò)以下方式解決了這些問(wèn)題:
在示例應(yīng)用中,IMyDependency 接口定義了服務(wù)為應(yīng)用提供的方法:
C#
public interface IMyDependency
{
Task WriteMessage(string message);
}
此接口由具體類(lèi)型 MyDependency 實(shí)現(xiàn):
C#
public class MyDependency : IMyDependency
{
private readonly ILogger<MyDependency> _logger;
public MyDependency(ILogger<MyDependency> logger)
{
_logger = logger;
}
public Task WriteMessage(string message)
{
_logger.LogInformation(
"MyDependency.WriteMessage called. Message: {MESSAGE}",
message);
return Task.FromResult(0);
}
}
MyDependency 在其構(gòu)造函數(shù)中請(qǐng)求 ILogger<TCategoryName>。 以鏈?zhǔn)椒绞绞褂靡蕾?lài)關(guān)系注入并不罕見(jiàn)。 每個(gè)請(qǐng)求的依賴(lài)關(guān)系相應(yīng)地請(qǐng)求其自己的依賴(lài)關(guān)系。 容器解析圖中的依賴(lài)關(guān)系并返回完全解析的服務(wù)。 必須被解析的依賴(lài)關(guān)系的集合通常被稱(chēng)為“依賴(lài)關(guān)系樹(shù)”、“依賴(lài)關(guān)系圖”或“對(duì)象圖”。
必須在服務(wù)容器中注冊(cè) IMyDependency 和 ILogger<TCategoryName>。 IMyDependency 已在 Startup.ConfigureServices 中注冊(cè)。 ILogger<TCategoryName> 由日志記錄抽象基礎(chǔ)結(jié)構(gòu)注冊(cè),因此它是框架默認(rèn)注冊(cè)的框架提供的服務(wù)。
容器通過(guò)利用(泛型)開(kāi)放類(lèi)型解析 ILogger<TCategoryName>,而無(wú)需注冊(cè)每個(gè)(泛型)構(gòu)造類(lèi)型:
C#
services.AddSingleton(typeof(ILogger<T>), typeof(Logger<T>));
在示例應(yīng)用中,使用具體類(lèi)型 MyDependency 注冊(cè) IMyDependency 服務(wù)。 注冊(cè)將服務(wù)生存期的范圍限定為單個(gè)請(qǐng)求的生存期。 本主題后面將介紹服務(wù)生存期。
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
// OperationService depends on each of the other Operation types.
services.AddTransient<OperationService, OperationService>();
}
備注
每個(gè) services.Add{SERVICE_NAME} 擴(kuò)展方法添加(并可能配置)服務(wù)。 例如,services.AddMvc() 添加 Razor Pages 和 MVC 需要的服務(wù)。 我們建議應(yīng)用遵循此約定。 將擴(kuò)展方法置于 Microsoft.Extensions.DependencyInjection 命名空間中以封裝服務(wù)注冊(cè)的組。
如果服務(wù)的構(gòu)造函數(shù)需要內(nèi)置類(lèi)型(如 string),則可以使用配置或選項(xiàng)模式注入該類(lèi)型:
C#
public class MyDependency : IMyDependency
{
public MyDependency(IConfiguration config)
{
var myStringValue = config["MyStringKey"];
// Use myStringValue
}
...
}
通過(guò)使用服務(wù)并分配給私有字段的類(lèi)的構(gòu)造函數(shù)請(qǐng)求服務(wù)的實(shí)例。 該字段用于在整個(gè)類(lèi)中根據(jù)需要訪(fǎng)問(wèn)服務(wù)。
在示例應(yīng)用中,請(qǐng)求 IMyDependency 實(shí)例并用于調(diào)用服務(wù)的 WriteMessage 方法:
C#
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;
public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}
public OperationService OperationService { get; }
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }
public async Task OnGetAsync()
{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
Startup.ConfigureServices 方法負(fù)責(zé)定義應(yīng)用使用的服務(wù),包括 Entity Framework Core 和 ASP.NET Core MVC 等平臺(tái)功能。 最初,提供給 ConfigureServices 的 IServiceCollection 定義了以下服務(wù)(具體取決于配置主機(jī)的方式):
當(dāng)服務(wù)集合擴(kuò)展方法可用于注冊(cè)服務(wù)(及其依賴(lài)服務(wù),如果需要)時(shí),約定使用單個(gè) Add{SERVICE_NAME} 擴(kuò)展方法來(lái)注冊(cè)該服務(wù)所需的所有服務(wù)。 以下代碼是如何使用擴(kuò)展方法 AddDbContext、AddIdentity 和 AddMvc 向容器添加其他服務(wù)的示例:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
有關(guān)詳細(xì)信息,請(qǐng)參閱 API 文檔中的 ServiceCollection 類(lèi)。
為每個(gè)注冊(cè)的服務(wù)選擇適當(dāng)?shù)纳嫫凇?nbsp;可以使用以下生存期配置 ASP.NET Core 服務(wù):
暫時(shí)
暫時(shí)生存期服務(wù)是每次從服務(wù)容器進(jìn)行請(qǐng)求時(shí)創(chuàng)建的。 這種生存期適合輕量級(jí)、 無(wú)狀態(tài)的服務(wù)。
作用域(Scoped)
作用域生存期服務(wù)以每個(gè)客戶(hù)端請(qǐng)求(連接)一次的方式創(chuàng)建。
警告
在中間件內(nèi)使用有作用域的服務(wù)時(shí),請(qǐng)將該服務(wù)注入至 Invoke 或 InvokeAsync 方法。 請(qǐng)不要通過(guò)構(gòu)造函數(shù)注入進(jìn)行注入,因?yàn)樗鼤?huì)強(qiáng)制服務(wù)的行為與單一實(shí)例類(lèi)似。 有關(guān)更多信息,請(qǐng)參見(jiàn)ASP.NET Core 中間件。
單例
單一實(shí)例生存期服務(wù)是在第一次請(qǐng)求時(shí)(或者在運(yùn)行 ConfigureServices 并且使用服務(wù)注冊(cè)指定實(shí)例時(shí))創(chuàng)建的。 每個(gè)后續(xù)請(qǐng)求都使用相同的實(shí)例。 如果應(yīng)用需要單一實(shí)例行為,建議允許服務(wù)容器管理服務(wù)的生存期。 不要實(shí)現(xiàn)單一實(shí)例設(shè)計(jì)模式并提供用戶(hù)代碼來(lái)管理對(duì)象在類(lèi)中的生存期。
警告
從單一實(shí)例解析有作用域的服務(wù)很危險(xiǎn)。 當(dāng)處理后續(xù)請(qǐng)求時(shí),它可能會(huì)導(dǎo)致服務(wù)處于不正確的狀態(tài)。
服務(wù)可以通過(guò)兩種機(jī)制來(lái)解析:
構(gòu)造函數(shù)可以接受依賴(lài)關(guān)系注入不提供的參數(shù),但參數(shù)必須分配默認(rèn)值。
當(dāng)服務(wù)由 IServiceProvider 或 ActivatorUtilities 解析時(shí),構(gòu)造函數(shù)注入需要 public 構(gòu)造函數(shù)。
當(dāng)服務(wù)由 ActivatorUtilities 解析時(shí),構(gòu)造函數(shù)注入要求只存在一個(gè)適用的構(gòu)造函數(shù)。 支持構(gòu)造函數(shù)重載,但其參數(shù)可以全部通過(guò)依賴(lài)注入來(lái)實(shí)現(xiàn)的重載只能存在一個(gè)。
通常使用設(shè)置了范圍的生存期將實(shí)體框架上下文添加到服務(wù)容器中,因?yàn)?Web 應(yīng)用數(shù)據(jù)庫(kù)操作通常將范圍設(shè)置為客戶(hù)端請(qǐng)求。 如果在注冊(cè)數(shù)據(jù)庫(kù)上下文時(shí),AddDbContext<TContext> 重載未指定生存期,則設(shè)置默認(rèn)生存期范圍。 給定生存期的服務(wù)不應(yīng)使用生存期比服務(wù)短的數(shù)據(jù)庫(kù)上下文。
為了演示生存期和注冊(cè)選項(xiàng)之間的差異,請(qǐng)考慮以下接口,將任務(wù)表示為具有唯一標(biāo)識(shí)符 OperationId 的操作。 根據(jù)為以下接口配置操作服務(wù)的生存期的方式,容器在類(lèi)請(qǐng)求時(shí)提供相同或不同的服務(wù)實(shí)例:
C#
public interface IOperation
{
Guid OperationId { get; }
}
public interface IOperationTransient : IOperation
{
}
public interface IOperationScoped : IOperation
{
}
public interface IOperationSingleton : IOperation
{
}
public interface IOperationSingletonInstance : IOperation
{
}
接口在 Operation 類(lèi)中實(shí)現(xiàn)。 Operation 構(gòu)造函數(shù)將生成一個(gè) GUID(如果未提供):
C#
public class Operation : IOperationTransient,
IOperationScoped,
IOperationSingleton,
IOperationSingletonInstance
{
public Operation() : this(Guid.NewGuid())
{
}
public Operation(Guid id)
{
OperationId = id;
}
public Guid OperationId { get; private set; }
}
注冊(cè) OperationService 取決于,每個(gè)其他 Operation 類(lèi)型。 當(dāng)通過(guò)依賴(lài)關(guān)系注入請(qǐng)求 OperationService 時(shí),它將接收每個(gè)服務(wù)的新實(shí)例或基于從屬服務(wù)的生存期的現(xiàn)有實(shí)例。
C#
public class OperationService
{
public OperationService(
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance instanceOperation)
{
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = instanceOperation;
}
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }
}
在 Startup.ConfigureServices 中,根據(jù)其指定的生存期,將每個(gè)類(lèi)型添加到容器中:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
// OperationService depends on each of the other Operation types.
services.AddTransient<OperationService, OperationService>();
}
IOperationSingletonInstance 服務(wù)正在使用已知 ID 為 Guid.Empty 的特定實(shí)例。 此類(lèi)型在使用時(shí)很明顯(其 GUID 全部為零)。
示例應(yīng)用演示了各個(gè)請(qǐng)求中和之間的對(duì)象生存期。 示例應(yīng)用的 IndexModel 請(qǐng)求每種 IOperation 類(lèi)型和 OperationService。 然后,頁(yè)面通過(guò)屬性分配顯示所有頁(yè)面模型類(lèi)和服務(wù)的 OperationId 值:
C#
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;
public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}
public OperationService OperationService { get; }
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }
public async Task OnGetAsync()
{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
以下兩個(gè)輸出顯示了兩個(gè)請(qǐng)求的結(jié)果:
第一個(gè)請(qǐng)求:
控制器操作:
暫時(shí)性:d233e165-f417-469b-a866-1cf1935d2518作用域:5d997e2d-55f5-4a64-8388-51c4e3a1ad19單一實(shí)例:01271bc1-9e31-48e7-8f7c-7261b040ded9實(shí)例:00000000-0000-0000-0000-000000000000
OperationService 操作:
暫時(shí)性:c6b049eb-1318-4e31-90f1-eb2dd849ff64作用域:5d997e2d-55f5-4a64-8388-51c4e3a1ad19單一實(shí)例:01271bc1-9e31-48e7-8f7c-7261b040ded9實(shí)例:00000000-0000-0000-0000-000000000000
第二個(gè)請(qǐng)求:
控制器操作:
暫時(shí)性:b63bd538-0a37-4ff1-90ba-081c5138dda0作用域:31e820c5-4834-4d22-83fc-a60118acb9f4單一實(shí)例:01271bc1-9e31-48e7-8f7c-7261b040ded9實(shí)例:00000000-0000-0000-0000-000000000000
OperationService 操作:
暫時(shí)性:c4cbacb8-36a2-436d-81c8-8c1b78808aaf作用域:31e820c5-4834-4d22-83fc-a60118acb9f4單一實(shí)例:01271bc1-9e31-48e7-8f7c-7261b040ded9實(shí)例:00000000-0000-0000-0000-000000000000
觀察哪個(gè) OperationId 值會(huì)在一個(gè)請(qǐng)求之內(nèi)和不同請(qǐng)求之間變化:
采用 IServiceScopeFactory.CreateScope 創(chuàng)建 IServiceScope ,以解析應(yīng)用作用域中有作用域的服務(wù)。此方法可以用于在啟動(dòng)時(shí)訪(fǎng)問(wèn)有作用域的服務(wù)以便運(yùn)行初始化任務(wù)。 以下示例演示如何在 Program.Main 中獲取 MyScopedService 的上下文:
C#
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var serviceContext = services.GetRequiredService<MyScopedService>();
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
}
host.Run();
}
如果在開(kāi)發(fā)環(huán)境中運(yùn)行應(yīng)用,默認(rèn)的服務(wù)提供程序會(huì)執(zhí)行檢查,從而確認(rèn)以下內(nèi)容:
調(diào)用 BuildServiceProvider 時(shí),會(huì)創(chuàng)建根服務(wù)提供程序。 在啟動(dòng)提供程序和應(yīng)用時(shí),根服務(wù)提供程序的生存期對(duì)應(yīng)于應(yīng)用/服務(wù)的生存期,并在關(guān)閉應(yīng)用時(shí)釋放。
有作用域的服務(wù)由創(chuàng)建它們的容器釋放。 如果作用域創(chuàng)建于根容器,則該服務(wù)的生存會(huì)有效地提升至單一實(shí)例,因?yàn)楦萜髦粫?huì)在應(yīng)用/服務(wù)關(guān)閉時(shí)將其釋放。 驗(yàn)證服務(wù)作用域,將在調(diào)用 BuildServiceProvider 時(shí)收集這類(lèi)情況。
有關(guān)更多信息,請(qǐng)參見(jiàn)ASP.NET Core Web 主機(jī)。
來(lái)自 HttpContext 的 ASP.NET Core 請(qǐng)求中可用的服務(wù)通過(guò) HttpContext.RequestServices 集合公開(kāi)。
請(qǐng)求服務(wù)表示作為應(yīng)用的一部分配置和請(qǐng)求的服務(wù)。 當(dāng)對(duì)象指定依賴(lài)關(guān)系時(shí),RequestServices(而不是 ApplicationServices)中的類(lèi)型將滿(mǎn)足這些要求。
通常,應(yīng)用不應(yīng)直接使用這些屬性。 相反,通過(guò)類(lèi)構(gòu)造函數(shù)請(qǐng)求類(lèi)所需的類(lèi)型,并允許框架注入依賴(lài)關(guān)系。 這樣生成的類(lèi)更易于測(cè)試。
備注
與訪(fǎng)問(wèn) RequestServices 集合相比,以構(gòu)造函數(shù)參數(shù)的形式請(qǐng)求依賴(lài)項(xiàng)是更優(yōu)先的選擇。
最佳做法是:
如果一個(gè)類(lèi)似乎有過(guò)多的注入依賴(lài)關(guān)系,這通常表明該類(lèi)擁有過(guò)多的責(zé)任并且違反了單一責(zé)任原則 (SRP)。 嘗試通過(guò)將某些職責(zé)移動(dòng)到一個(gè)新類(lèi)來(lái)重構(gòu)類(lèi)。 請(qǐng)記住,Razor Pages 頁(yè)模型類(lèi)和 MVC 控制器類(lèi)應(yīng)關(guān)注用戶(hù)界面問(wèn)題。 業(yè)務(wù)規(guī)則和數(shù)據(jù)訪(fǎng)問(wèn)實(shí)現(xiàn)細(xì)節(jié)應(yīng)保留在適用于這些分離的關(guān)注點(diǎn)的類(lèi)中。
容器為其創(chuàng)建的 IDisposable 類(lèi)型調(diào)用 Dispose。 如果通過(guò)用戶(hù)代碼將實(shí)例添加到容器中,則不會(huì)自動(dòng)處理該實(shí)例。
C#
// Services that implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}
public interface ISomeService {}
public class SomeServiceImplementation : ISomeService, IDisposable {}
public void ConfigureServices(IServiceCollection services)
{
// The container creates the following instances and disposes them automatically:
services.AddScoped<Service1>();
services.AddSingleton<Service2>();
services.AddSingleton<ISomeService>(sp => new SomeServiceImplementation());
// The container doesn't create the following instances, so it doesn't dispose of
// the instances automatically:
services.AddSingleton<Service3>(new Service3());
services.AddSingleton(new Service3());
}
內(nèi)置的服務(wù)容器旨在滿(mǎn)足框架和大多數(shù)消費(fèi)者應(yīng)用的需求。 我們建議使用內(nèi)置容器,除非你需要的特定功能不受它支持。 內(nèi)置容器中找不到第三方容器支持的某些功能:
有關(guān)支持適配器的部分容器列表,請(qǐng)參閱依賴(lài)關(guān)系注入 readme.md 文件。
以下示例將內(nèi)置容器替換為 Autofac:
在運(yùn)行時(shí),使用 Autofac 來(lái)解析類(lèi)型,并注入依賴(lài)關(guān)系。 要了解有關(guān)結(jié)合使用 Autofac 和 ASP.NET Core 的詳細(xì)信息,請(qǐng)參閱 Autofac 文檔。
創(chuàng)建線(xiàn)程安全的單一實(shí)例服務(wù)。 如果單例服務(wù)依賴(lài)于一個(gè)瞬時(shí)服務(wù),那么瞬時(shí)服務(wù)可能也需要線(xiàn)程安全,具體取決于單例使用它的方式。
單個(gè)服務(wù)的工廠方法,例如 AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) 的第二個(gè)參數(shù),不需要是線(xiàn)程安全的。 像類(lèi)型 (static) 構(gòu)造函數(shù)一樣,它保證由單個(gè)線(xiàn)程調(diào)用一次。
像任何一組建議一樣,你可能會(huì)遇到需要忽略某建議的情況。 例外情況很少見(jiàn) — 主要是框架本身內(nèi)部的特殊情況。
DI 是靜態(tài)/全局對(duì)象訪(fǎng)問(wèn)模式的替代方法。 如果將其與靜態(tài)對(duì)象訪(fǎng)問(wèn)混合使用,則可能無(wú)法實(shí)現(xiàn) DI 的優(yōu)點(diǎn)。
更多建議: