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