ASP.NET Core基礎知識(三)【依賴關係注入(服務)】

風靈使發表於2019-02-18

ASP.NET Core 依賴注入

ASP.NET Core 支援依賴關係注入 (DI) 軟體設計模式,這是一種在類及其依賴關係之間實現控制反轉 (IoC)的技術。

有關特定於 MVC 控制器中依賴關係注入的詳細資訊,請參閱 在 ASP.NET Core 中將依賴項注入到控制器

依賴關係注入概述

依賴項是另一個物件所需的任何物件。 使用應用中其他類所依賴的 WriteMessage 方法檢查以下 MyDependency 類:

public class MyDependency
{
    public MyDependency()
    {
    }

    public Task WriteMessage(string message)
    {
        Console.WriteLine(
            $"MyDependency.WriteMessage called. Message: {message}");

        return Task.FromResult(0);
    }
}

可以建立 MyDependency 類的例項以使 WriteMessage 方法可用於類。 MyDependency 類是 IndexModel 類的依賴項:

public class IndexModel : PageModel
{
    MyDependency _dependency = new MyDependency();

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

可以建立 MyDependency 類的例項以使 WriteMessage 方法可用於類。 MyDependency 類是 HomeController 類的依賴項:

public class HomeController : Controller
{
    MyDependency _dependency = new MyDependency();

    public async Task<IActionResult> Index()
    {
        await _dependency.WriteMessage(
            "HomeController.Index created this message.");

        return View();
    }
}

該類建立並直接依賴於 MyDependency 例項。 程式碼依賴關係(如前面的示例)存在問題,應該避免使用,原因如下:

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

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

  • 使用介面抽象化依賴關係實現。
  • 註冊服務容器中的依賴關係。 ASP.NET Core 提供了一個內建的服務容器IServiceProvider。 服務已在應用的 Startup.ConfigureServices 方法中註冊。
  • 將服務注入到使用它的類的建構函式中。 框架負責建立依賴關係的例項,並在不再需要時對其進行處理。

示例應用中,IMyDependency 介面定義了服務為應用提供的方法:

public interface IMyDependency
{
    Task WriteMessage(string message);
}

此介面由具體型別 MyDependency 實現:

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 在其建構函式中請求ILogger<TCategoryName>。 以鏈式方式使用依賴關係注入並不罕見。 每個請求的依賴關係相應地請求其自己的依賴關係。 容器解析圖中的依賴關係並返回完全解析的服務。 必須被解析的依賴關係的集合通常被稱為“依賴關係樹”、“依賴關係圖”或“物件圖”。

必須在服務容器中註冊 IMyDependencyILogger<TCategoryName>IMyDependency 已在 Startup.ConfigureServices 中註冊。 ILogger<TCategoryName> 由日誌記錄抽象基礎結構註冊,因此它是框架預設註冊的框架提供的服務

在示例應用中,使用具體型別 MyDependency 註冊 IMyDependency 服務。 註冊將服務生存期的範圍限定為單個請求的生存期。 本主題後面將介紹服務生存期

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    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 名稱空間中以封裝服務註冊的組。

如果服務的建構函式需要基元(如 string),則可以使用配置選項模式注入基元:

public class MyDependency : IMyDependency
{
    public MyDependency(IConfiguration config)
    {
        var myStringValue = config["MyStringKey"];

        // Use myStringValue
    }

    ...
}

通過使用服務並分配給私有欄位的類的建構函式請求服務的例項。 該欄位用於在整個類中根據需要訪問服務。

在示例應用中,請求 IMyDependency 例項並用於呼叫服務的 WriteMessage 方法:

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 CoreASP.NET Core MVC 等平臺功能。 最初,提供給 ConfigureServicesIServiceCollection 定義了以下服務(具體取決於配置主機的方式):

服務型別 生存期
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} 擴充套件方法來註冊該服務所需的所有服務。 以下程式碼是如何使用擴充套件方法 AddDbContextAddIdentityAddMvc向容器新增其他服務的示例:

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 類

服務生存期

為每個註冊的服務選擇適當的生存期。 可以使用以下生存期配置 ASP.NET Core 服務:

暫時

暫時生存期服務是每次請求時建立的。 這種生存期適合輕量級、 無狀態的服務。

作用域(Scoped)

作用域生存期服務以每個請求一次的方式建立。

警告
在中介軟體內使用有作用域的服務時,請將該服務注入至 InvokeInvokeAsync 方法。 請不要通過建構函式注入進行注入,因為它會強制服務的行為與單一例項類似。 有關更多資訊,請參見ASP.NET Core 中介軟體

單例

單一例項生存期服務是在第一次請求時(或者在執行 ConfigureServices 並且使用服務註冊指定例項時)建立的。 每個後續請求都使用相同的例項。 如果應用需要單一例項行為,建議允許服務容器管理服務的生存期。不要實現單一例項設計模式並提供使用者程式碼來管理物件在類中的生存期。

警告
從單一例項解析有作用域的服務很危險。 當處理後續請求時,它可能會導致服務處於不正確的狀態。

建構函式注入行為

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

  • IServiceProvider
  • ActivatorUtilities 允許在依賴關係注入容器中建立沒有服務註冊的物件。 ActivatorUtilities 用於面向使用者的抽象,例如標記幫助器、MVC 控制器和模型繫結器。

建構函式可以接受依賴關係注入不提供的引數,但引數必須分配預設值。

當服務由 IServiceProviderActivatorUtilities 解析時,建構函式注入需要 public 建構函式。

當服務由 ActivatorUtilities 解析時,建構函式注入要求只存在一個適用的建構函式。 支援建構函式過載,但其引數可以全部通過依賴注入來實現的過載只能存在一個。

實體框架上下文

應使用有作用域的生存期將實體框架上下文新增到服務容器中。 在註冊資料庫上下文時,通過呼叫AddDbContext 方法自動處理。 使用資料庫上下文的服務也應使用有作用域的生存期。

生存期和註冊選項

為了演示生存期和註冊選項之間的差異,請考慮以下介面,將任務表示為具有唯一識別符號 OperationId 的操作。 根據為以下介面配置操作服務的生存期的方式,容器在類請求時提供相同或不同的服務例項:

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

介面在 Operation 類中實現。 Operation 建構函式將生成一個 GUID(如果未提供):

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 時,它將接收每個服務的新例項或基於從屬服務的生存期的現有例項。

  • 如果在請求時建立了臨時服務,則 IOperationTransient 服務的 OperationIdOperationServiceOperationId 不同。 OperationService 將接收 IOperationTransient 類的新例項。 新例項將生成一個不同的 OperationId
  • 如果按請求建立有作用域的服務,則 IOperationScoped 服務的 OperationId 與請求中 OperationService 的該 ID 相同。 在請求中,兩個服務共享不同的 OperationId 值。
  • 如果單一資料庫和單一例項服務只建立一次並在所有請求和所有服務中使用,則 OperationId 在所有服務請求中保持不變。
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 中,根據其指定的生存期,將每個型別新增到容器中:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    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 值:

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建立IServiceScope,以解析應用作用域中有作用域的服務。 此方法可以用於在啟動時訪問有作用域的服務以便執行初始化任務。 以下示例演示如何在 Program.Main 中獲取 MyScopedService 的上下文:

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();
}

作用域驗證

如果在開發環境中執行應用,預設的服務提供程式會執行檢查,從而確認以下內容:

  • 沒有從根服務提供程式直接或間接解析到有作用域的服務。
  • 未將有作用域的服務直接或間接注入到單一例項。

呼叫 BuildServiceProvider 時,會建立根服務提供程式。 在啟動提供程式和應用時,根服務提供程式的生存期對應於應用/服務的生存期,並在關閉應用時釋放。

有作用域的服務由建立它們的容器釋放。 如果作用域建立於根容器,則該服務的生存會有效地提升至單一例項,因為根容器只會在應用/服務關閉時將其釋放。 驗證服務作用域,將在呼叫 BuildServiceProvider 時收集這類情況。

有關更多資訊,請參見ASP.NET Core Web 主機

請求服務

來自 HttpContextASP.NET Core 請求中可用的服務通過HttpContext.RequestServices集合公開。

請求服務表示作為應用的一部分配置和請求的服務。 當物件指定依賴關係時,RequestServices(而不是 ApplicationServices)中的型別將滿足這些要求。

通常,應用不應直接使用這些屬性。 相反,通過類建構函式請求類所需的型別,並允許框架注入依賴關係。 這樣生成的類更易於測試。

備註
與訪問 RequestServices 集合相比,以建構函式引數的形式請求依賴項是更優先的選擇。

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

最佳做法是:

  • 設計服務以使用依賴關係注入來獲取其依賴關係。
  • 避免有狀態的靜態方法呼叫(稱為靜態粘附的實踐)。
  • 避免在服務中直接例項化依賴類。 直接例項化將程式碼耦合到特定實現。

遵循物件導向設計的 SOLID 原則,往往會獲得構造良好且易於測試的小型類。

如果一個類似乎有過多的注入依賴關係,這通常表明該類擁有過多的責任並且違反了單一責任原則 (SRP)。 嘗試通過將某些職責移動到一個新類來重構類。 請記住,Razor Pages 頁模型類和 MVC 控制器類應關注使用者介面問題。 業務規則和資料訪問實現細節應保留在適用於這些分離的關注點的類中。

服務處理

容器為其建立的 IDisposable 型別呼叫 Dispose。 如果通過使用者程式碼將例項新增到容器中,則不會自動處理該例項。

// 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());
}

預設服務容器替換

內建的服務容器旨在滿足框架和大多數消費者應用的需求。 我們建議使用內建容器,除非你需要的特定功能不受它支援。 內建容器中找不到第三方容器支援的某些功能:

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

有關支援介面卡的部分容器列表,請參閱依賴關係注入 readme.md 檔案

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

  • 安裝適當的容器包:

  • Startup.ConfigureServices 中配置容器並返回 IServiceProvider

    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

    public class DefaultModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
        }
    }
    

在執行時,使用 Autofac 來解析型別,並注入依賴關係。 要了解有關結合使用 Autofac 和 ASP.NET Core 的詳細資訊,請參閱 Autofac 文件

執行緒安全

單例服務需要是執行緒安全的。 如果單例服務依賴於一個瞬時服務,那麼瞬時服務可能也需要是執行緒安全的,具體取決於單例使用它的方式。

單個服務的工廠方法,例如AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) 的第二個引數,不需要是執行緒安全的。 像型別 (static) 建構函式一樣,它保證由單個執行緒呼叫一次。

建議

  • 不支援基於 async/awaitTask 的服務解析。 C# 不支援非同步建構函式,因此推薦的模式是在同步解析服務後使用非同步方法。

  • 避免在服務容器中直接儲存資料和配置。 例如,使用者的購物車通常不應新增到服務容器中。 配置應使用 選項模型。 同樣,避免"資料持有者"物件,也就是僅僅為實現對某些其他物件的訪問而存在的物件。 最好通過 DI 請求實際專案。

  • 避免靜態訪問服務(例如,靜態鍵入 IApplicationBuilder.ApplicationServices以便在其他地方使用)。

  • 避免使用服務定位器模式。 例如,可以改為使用 DI 時,不要呼叫 GetService來獲取服務例項。 要避免的另一個服務定位器變體是注入可在執行時解析依賴項的工廠。 這兩種做法混合了控制反轉策略。

  • 避免靜態訪問 HttpContext(例如,IHttpContextAccessor.HttpContext)。

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

DI 是靜態/全域性物件訪問模式的替代方法。 如果將其與靜態物件訪問混合使用,則可能無法實現 DI 的優點。

相關文章