ASP.NET Core 支持依賴關系注入 (DI) 軟件設計模式,這是一種在類及其依賴關系之間實現(xiàn)控制反轉 (IoC) 的技術。
有關特定于 MVC 控制器中依賴關系注入的詳細信息,請參閱 在 ASP.NET Core 中將依賴項注入到控制器。
依賴項是另一個對象所需的任何對象。 使用應用中其他類所依賴的 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 實例。 代碼依賴關系(如前面的示例)存在問題,應該避免使用,原因如下:
依賴關系注入通過以下方式解決了這些問題:
在示例應用中,IMyDependency 接口定義了服務為應用提供的方法:
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 在其構造函數(shù)中請求 ILogger<TCategoryName>。 以鏈式方式使用依賴關系注入并不罕見。 每個請求的依賴關系相應地請求其自己的依賴關系。 容器解析圖中的依賴關系并返回完全解析的服務。 必須被解析的依賴關系的集合通常被稱為“依賴關系樹”、“依賴關系圖”或“對象圖”。
必須在服務容器中注冊 IMyDependency 和 ILogger<TCategoryName>。 IMyDependency 已在 Startup.ConfigureServices 中注冊。 ILogger<TCategoryName> 由日志記錄抽象基礎結構注冊,因此它是框架默認注冊的框架提供的服務。
容器通過利用(泛型)開放類型解析 ILogger<TCategoryName>,而無需注冊每個(泛型)構造類型:
C#
services.AddSingleton(typeof(ILogger<T>), typeof(Logger<T>));
在示例應用中,使用具體類型 MyDependency 注冊 IMyDependency 服務。 注冊將服務生存期的范圍限定為單個請求的生存期。 本主題后面將介紹服務生存期。
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} 擴展方法添加(并可能配置)服務。 例如,services.AddMvc() 添加 Razor Pages 和 MVC 需要的服務。 我們建議應用遵循此約定。 將擴展方法置于 Microsoft.Extensions.DependencyInjection 命名空間中以封裝服務注冊的組。
如果服務的構造函數(shù)需要內置類型(如 string),則可以使用配置或選項模式注入該類型:
C#
public class MyDependency : IMyDependency
{
public MyDependency(IConfiguration config)
{
var myStringValue = config["MyStringKey"];
// Use myStringValue
}
...
}
通過使用服務并分配給私有字段的類的構造函數(shù)請求服務的實例。 該字段用于在整個類中根據(jù)需要訪問服務。
在示例應用中,請求 IMyDependency 實例并用于調用服務的 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 方法負責定義應用使用的服務,包括 Entity Framework Core 和 ASP.NET Core MVC 等平臺功能。 最初,提供給 ConfigureServices 的 IServiceCollection 定義了以下服務(具體取決于配置主機的方式):
當服務集合擴展方法可用于注冊服務(及其依賴服務,如果需要)時,約定使用單個 Add{SERVICE_NAME} 擴展方法來注冊該服務所需的所有服務。 以下代碼是如何使用擴展方法 AddDbContext、AddIdentity 和 AddMvc 向容器添加其他服務的示例:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
有關詳細信息,請參閱 API 文檔中的 ServiceCollection 類。
為每個注冊的服務選擇適當?shù)纳嫫凇?nbsp;可以使用以下生存期配置 ASP.NET Core 服務:
暫時
暫時生存期服務是每次從服務容器進行請求時創(chuàng)建的。 這種生存期適合輕量級、 無狀態(tài)的服務。
作用域(Scoped)
作用域生存期服務以每個客戶端請求(連接)一次的方式創(chuàng)建。
警告
在中間件內使用有作用域的服務時,請將該服務注入至 Invoke 或 InvokeAsync 方法。 請不要通過構造函數(shù)注入進行注入,因為它會強制服務的行為與單一實例類似。 有關更多信息,請參見ASP.NET Core 中間件。
單例
單一實例生存期服務是在第一次請求時(或者在運行 ConfigureServices 并且使用服務注冊指定實例時)創(chuàng)建的。 每個后續(xù)請求都使用相同的實例。 如果應用需要單一實例行為,建議允許服務容器管理服務的生存期。 不要實現(xiàn)單一實例設計模式并提供用戶代碼來管理對象在類中的生存期。
警告
從單一實例解析有作用域的服務很危險。 當處理后續(xù)請求時,它可能會導致服務處于不正確的狀態(tài)。
服務可以通過兩種機制來解析:
構造函數(shù)可以接受依賴關系注入不提供的參數(shù),但參數(shù)必須分配默認值。
當服務由 IServiceProvider 或 ActivatorUtilities 解析時,構造函數(shù)注入需要 public 構造函數(shù)。
當服務由 ActivatorUtilities 解析時,構造函數(shù)注入要求只存在一個適用的構造函數(shù)。 支持構造函數(shù)重載,但其參數(shù)可以全部通過依賴注入來實現(xiàn)的重載只能存在一個。
通常使用設置了范圍的生存期將實體框架上下文添加到服務容器中,因為 Web 應用數(shù)據(jù)庫操作通常將范圍設置為客戶端請求。 如果在注冊數(shù)據(jù)庫上下文時,AddDbContext<TContext> 重載未指定生存期,則設置默認生存期范圍。 給定生存期的服務不應使用生存期比服務短的數(shù)據(jù)庫上下文。
為了演示生存期和注冊選項之間的差異,請考慮以下接口,將任務表示為具有唯一標識符 OperationId 的操作。 根據(jù)為以下接口配置操作服務的生存期的方式,容器在類請求時提供相同或不同的服務實例:
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 構造函數(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 類型。 當通過依賴關系注入請求 OperationService 時,它將接收每個服務的新實例或基于從屬服務的生存期的現(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 服務正在使用已知 ID 為 Guid.Empty 的特定實例。 此類型在使用時很明顯(其 GUID 全部為零)。
示例應用演示了各個請求中和之間的對象生存期。 示例應用的 IndexModel 請求每種 IOperation 類型和 OperationService。 然后,頁面通過屬性分配顯示所有頁面模型類和服務的 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.");
}
}
以下兩個輸出顯示了兩個請求的結果:
第一個請求:
控制器操作:
暫時性: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 值會在一個請求之內和不同請求之間變化:
采用 IServiceScopeFactory.CreateScope 創(chuàng)建 IServiceScope ,以解析應用作用域中有作用域的服務。此方法可以用于在啟動時訪問有作用域的服務以便運行初始化任務。 以下示例演示如何在 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)境中運行應用,默認的服務提供程序會執(zhí)行檢查,從而確認以下內容:
調用 BuildServiceProvider 時,會創(chuàng)建根服務提供程序。 在啟動提供程序和應用時,根服務提供程序的生存期對應于應用/服務的生存期,并在關閉應用時釋放。
有作用域的服務由創(chuàng)建它們的容器釋放。 如果作用域創(chuàng)建于根容器,則該服務的生存會有效地提升至單一實例,因為根容器只會在應用/服務關閉時將其釋放。 驗證服務作用域,將在調用 BuildServiceProvider 時收集這類情況。
有關更多信息,請參見ASP.NET Core Web 主機。
來自 HttpContext 的 ASP.NET Core 請求中可用的服務通過 HttpContext.RequestServices 集合公開。
請求服務表示作為應用的一部分配置和請求的服務。 當對象指定依賴關系時,RequestServices(而不是 ApplicationServices)中的類型將滿足這些要求。
通常,應用不應直接使用這些屬性。 相反,通過類構造函數(shù)請求類所需的類型,并允許框架注入依賴關系。 這樣生成的類更易于測試。
備注
與訪問 RequestServices 集合相比,以構造函數(shù)參數(shù)的形式請求依賴項是更優(yōu)先的選擇。
最佳做法是:
如果一個類似乎有過多的注入依賴關系,這通常表明該類擁有過多的責任并且違反了單一責任原則 (SRP)。 嘗試通過將某些職責移動到一個新類來重構類。 請記住,Razor Pages 頁模型類和 MVC 控制器類應關注用戶界面問題。 業(yè)務規(guī)則和數(shù)據(jù)訪問實現(xiàn)細節(jié)應保留在適用于這些分離的關注點的類中。
容器為其創(chuàng)建的 IDisposable 類型調用 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());
}
內置的服務容器旨在滿足框架和大多數(shù)消費者應用的需求。 我們建議使用內置容器,除非你需要的特定功能不受它支持。 內置容器中找不到第三方容器支持的某些功能:
有關支持適配器的部分容器列表,請參閱依賴關系注入 readme.md 文件。
以下示例將內置容器替換為 Autofac:
在運行時,使用 Autofac 來解析類型,并注入依賴關系。 要了解有關結合使用 Autofac 和 ASP.NET Core 的詳細信息,請參閱 Autofac 文檔。
創(chuàng)建線程安全的單一實例服務。 如果單例服務依賴于一個瞬時服務,那么瞬時服務可能也需要線程安全,具體取決于單例使用它的方式。
單個服務的工廠方法,例如 AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) 的第二個參數(shù),不需要是線程安全的。 像類型 (static) 構造函數(shù)一樣,它保證由單個線程調用一次。
像任何一組建議一樣,你可能會遇到需要忽略某建議的情況。 例外情況很少見 — 主要是框架本身內部的特殊情況。
DI 是靜態(tài)/全局對象訪問模式的替代方法。 如果將其與靜態(tài)對象訪問混合使用,則可能無法實現(xiàn) DI 的優(yōu)點。
更多建議: