在 ASP.NET Core 依賴注入

2019-04-17 08:58 更新

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 實例。 代碼依賴關系(如前面的示例)存在問題,應該避免使用,原因如下:

  • 要用不同的實現(xiàn)替換 MyDependency,必須修改類。
  • 如果 MyDependency 具有依賴關系,則必須由類對其進行配置。 在具有多個依賴于 MyDependency 的類的大型項目中,配置代碼在整個應用中會變得分散。
  • 這種實現(xiàn)很難進行單元測試。 應用應使用模擬或存根 MyDependency 類,該類不能使用此方法。

依賴關系注入通過以下方式解決了這些問題:

  • 使用接口抽象化依賴關系實現(xiàn)。
  • 注冊服務容器中的依賴關系。 ASP.NET Core 提供了一個內置的服務容器 IServiceProvider。 服務已在應用的 Startup.ConfigureServices 方法中注冊。
  • 將服務注入到使用它的類的構造函數(shù)中。 框架負責創(chuàng)建依賴關系的實例,并在不再需要時對其進行處理。

示例應用中,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 定義了以下服務(具體取決于配置主機的方式):

服務類型生存期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory暫時
Microsoft.AspNetCore.Hosting.IApplicationLifetime單例
Microsoft.AspNetCore.Hosting.IHostingEnvironment單例
Microsoft.AspNetCore.Hosting.IStartup單例
Microsoft.AspNetCore.Hosting.IStartupFilter暫時
Microsoft.AspNetCore.Hosting.Server.IServer單例
Microsoft.AspNetCore.Http.IHttpContextFactory暫時
Microsoft.Extensions.Logging.ILogger<T>單例
Microsoft.Extensions.Logging.ILoggerFactory單例
Microsoft.Extensions.ObjectPool.ObjectPoolProvider單一實例
Microsoft.Extensions.Options.IConfigureOptions<T>暫時
Microsoft.Extensions.Options.IOptions<T>單一實例
System.Diagnostics.DiagnosticSource單一實例
System.Diagnostics.DiagnosticListener單例

當服務集合擴展方法可用于注冊服務(及其依賴服務,如果需要)時,約定使用單個 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ù)注入行為

服務可以通過兩種機制來解析:

  • IServiceProvider
  • ActivatorUtilities – 允許在依賴關系注入容器中創(chuàng)建沒有服務注冊的對象。 ActivatorUtilities 用于面向用戶的抽象,例如標記幫助器、MVC 控制器和模型綁定器。

構造函數(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)有實例。

  • 如果從容器請求時創(chuàng)建了臨時服務,則 IOperationTransient 服務的 OperationId 與 OperationService 的 OperationId 不同。 OperationService 將接收 IOperationTransient 類的新實例。 新實例將生成一個不同的 OperationId。
  • 如果按客戶端請求創(chuàng)建有作用域的服務,則 IOperationScoped 服務的 OperationId 與客戶端請求中 OperationService 的該 ID 相同。 在客戶端請求中,兩個服務共享不同的 OperationId 值。
  • 如果單一數(shù)據(jù)庫和單一實例服務只創(chuàng)建一次并在所有客戶端請求和所有服務中使用,則 OperationId 在所有服務請求中保持不變。

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 值會在一個請求之內和不同請求之間變化:

  • 暫時性對象始終不同。 第一個和第二個客戶端請求的暫時性 OperationId 值對于 OperationService 操作和在客戶端請求內都是不同的。 為每個服務請求和客戶端請求提供了一個新實例。
  • 作用域對象在一個客戶端請求中是相同的,但在多個客戶端請求中是不同的。
  • 單一實例對象對每個對象和每個請求都是相同的(不管 ConfigureServices 中是否提供 Operation 實例)。

從 main 調用服務

采用 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)先的選擇。

設計能夠進行依賴關系注入的服務

最佳做法是:

  • 設計服務以使用依賴關系注入來獲取其依賴關系。
  • 避免進行有狀態(tài)的靜態(tài)方法調用。
  • 避免在服務中直接實例化依賴類。 直接實例化將代碼耦合到特定實現(xiàn)。
  • 不在應用類中包含過多內容,確保設計規(guī)范,并易于測試。

如果一個類似乎有過多的注入依賴關系,這通常表明該類擁有過多的責任并且違反了單一責任原則 (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ù)消費者應用的需求。 我們建議使用內置容器,除非你需要的特定功能不受它支持。 內置容器中找不到第三方容器支持的某些功能:

  • 屬性注入
  • 基于名稱的注入
  • 子容器
  • 自定義生存期管理
  • 對遲緩初始化的 Func<T> 支持

有關支持適配器的部分容器列表,請參閱依賴關系注入 readme.md 文件

以下示例將內置容器替換為 Autofac

  • 安裝適當?shù)娜萜靼?a rel="external nofollow" target="_blank" >AutofacAutofac.Extensions.DependencyInjection
  • 在 Startup.ConfigureServices 中配置容器并返回 IServiceProvider:C#復制public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); // Add other framework services // Add Autofac var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule<DefaultModule>(); containerBuilder.Populate(services); var container = containerBuilder.Build(); return new AutofacServiceProvider(container); } 要使用第三方容器,Startup.ConfigureServices 必須返回 IServiceProvider。
  • 在 DefaultModule 中配置 Autofac:C#復制public class DefaultModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<CharacterRepository>().As<ICharacterRepository>(); } }

在運行時,使用 Autofac 來解析類型,并注入依賴關系。 要了解有關結合使用 Autofac 和 ASP.NET Core 的詳細信息,請參閱 Autofac 文檔。

線程安全

創(chuàng)建線程安全的單一實例服務。 如果單例服務依賴于一個瞬時服務,那么瞬時服務可能也需要線程安全,具體取決于單例使用它的方式。

單個服務的工廠方法,例如 AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) 的第二個參數(shù),不需要是線程安全的。 像類型 (static) 構造函數(shù)一樣,它保證由單個線程調用一次。

建議

  • 不支持基于 async/await 和 Task 的服務解析。 C# 不支持異步構造函數(shù),因此推薦的模式是在同步解析服務后使用異步方法。
  • 避免在服務容器中直接存儲數(shù)據(jù)和配置。 例如,用戶的購物車通常不應添加到服務容器中。 配置應使用 選項模型。 同樣,避免"數(shù)據(jù)持有者"對象,也就是僅僅為實現(xiàn)對某些其他對象的訪問而存在的對象。 最好通過 DI 請求實際項目。
  • 避免靜態(tài)訪問服務(例如,靜態(tài)鍵入 IApplicationBuilder.ApplicationServices 以便在其他地方使用)。
  • 避免使用服務定位器模式。 例如,可以改為使用 DI 時,不要調用 GetService 來獲取服務實例。 要避免的另一個服務定位器變體是注入可在運行時解析依賴項的工廠。 這兩種做法混合了控制反轉策略。
  • 避免靜態(tài)訪問 HttpContext(例如,IHttpContextAccessor.HttpContext)。

像任何一組建議一樣,你可能會遇到需要忽略某建議的情況。 例外情況很少見 — 主要是框架本身內部的特殊情況。

DI 是靜態(tài)/全局對象訪問模式的替代方法。 如果將其與靜態(tài)對象訪問混合使用,則可能無法實現(xiàn) DI 的優(yōu)點。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號