ASP.NET Core 依賴注入(DI)簡介


本文為官方文檔譯文

ASP.NET Core是從根本上設計來支持和利用依賴注入。 ASP.NET Core應用程序可以通過將其注入到Startup類中的方法中來利用內置的框架服務,並且應用程序服務也可以配置為注入。 ASP.NET Core提供的默認服務容器提供了一個最小的功能集,而不是替換其他容器。

什么是依賴注入?

依賴注入,英文是Dependency Injection一般簡稱DI,是實現對象與其協作者或依賴關系之間松散耦合的技術。為了執行其操作,類所需的對象不是直接實例化協作者或使用靜態引用,而是以某種方式提供給類。 大多數情況下,類將通過它們的構造函數來聲明它們的依賴關系,允許它們遵循顯式依賴原則。 這種方法被稱為“構造方法注入”。

在設計時考慮到DI,它們更加松散耦合,因為他們沒有直接的,硬編碼的依賴於他們的合作者。 這遵循依賴性反轉原則,其中指出“高級模塊不應該依賴於低級模塊;兩者都應該取決於抽象”。 除了引用特定的實現之外,類請求構造類時提供給它們的抽象(通常是接口)。 將依賴關系提取到接口中並將這些接口的實現提供為參數也是策略設計模式的一個示例。

當系統被設計為使用DI時,有許多類通過它們的構造方法(或屬性)請求它們的依賴關系,有一個專門用於創建這些類及其關聯的依賴關系的類是有幫助的。 這些類被稱為容器,或更具體地稱為控制反轉(IoC)容器或依賴注入(DI)容器。 容器本質上是一個工廠,負責提供從它請求的類型的實例。 如果給定類型已聲明它具有依賴關系,並且容器已配置為提供依賴關系類型,那么它將創建依賴關系作為創建請求的實例的一部分。 以這種方式,可以將復雜的依賴關系圖提供給類,而不需要任何硬編碼的對象構造。 除了創建具有依賴關系的對象之外,容器通常會在應用程序中管理對象生命周期。

ASP.NET Core包括一個簡單的內置容器(由IServiceProvider接口表示),默認情況下支持構造函數注入,ASP.NET通過DI可以提供某些服務。 ASP.NET的容器是指它作為服務管理的類型。 在本文的其余部分中,服務將引用由ASP.NET Core的IoC容器管理的類型。 您可以在應用程序的Startup類中的ConfigureServices方法中配置內置容器的服務。

本文介紹依賴注入,因為它適用於所有ASP.NET應用程序。 依賴注入和控制器涵蓋MVC控制器內的依賴注入。

推薦Martin Fowler的文章:Inversion of Control Containers and the Dependency Injection Pattern

構造器注入

構造器注入要求所討論的構造方法是公開的。 否則,你的應用程序會拋出InvalidOperationException

不能找到類型“xxx”的合適的構造函數。 確保類型是具體的,服務是為公共構造函數的所有參數注冊的。

構造器注入需要只存在一個適用的構造函數。 支持構造函數重載,但只有一個重載可以存在,其參數都可以通過依賴注入來實現。 如果有多個存在,您的應用程序將拋出一個InvalidOperationException

接受所有給定參數類型的多個構造函數已在類型'xxxx'中找到。 應該只有一個適用的構造函數。

構造方法可以接受非依賴注入提供的參數,但這些參數必須支持默認值。 例如:

// throws InvalidOperationException: Unable to resolve service for type 'System.String'...
public CharactersController(ICharacterRepository characterRepository, string title)
{
    _characterRepository = characterRepository;
    _title = title;
}

// runs without error
public CharactersController(ICharacterRepository characterRepository, string title = "Characters")
{
    _characterRepository = characterRepository;
    _title = title;
}

使用框架提供的服務

Startup類中的ConfigureServices方法負責定義應用程序將使用的服務,包括平台功能,如Entity Framework Core和ASP.NET Core MVC。 最初,提供給ConfigureServices的IServiceCollection具有定義的以下服務(取決於如何配置Host):

服務類型 生命周期
Microsoft.AspNetCore.Hosting.IHostingEnvironment Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.Logging.ILogger Singleton
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transient
Microsoft.AspNetCore.Http.IHttpContextFactory Transient
Microsoft.Extensions.Options.IOptions Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transient
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions Transient
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IApplicationLifetime Singleton

以下是使用多個擴展方法(如AddDbContextAddIdentityAddMvc)向容器添加附加服務的示例。

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
}

由ASP.NET提供的功能和中間件(如MVC)遵循使用單個AddServiceName擴展方法注冊該功能所需的所有服務的約定。

您可以通過參數列表在Startup方法中請求某些框架提供的服務 .

注冊自己的服務

您可以注冊自己的應用程序服務,如下所示。 第一個通用類型表示將從容器請求的類型(通常為接口)。 第二個通用類型表示將由容器實例化並用於實現這種請求的具體類型。

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();

每個services.Add<ServiceName>擴展方法添加(並可能配置)服務。 例如,services.AddMvc()添加了MVC需要的服務。 建議您遵循此約定,將擴展方法放在Microsoft.Extensions.DependencyInjection命名空間中,以封裝服務注冊組。

AddTransient方法用於將抽象類型映射到為需要的每個對象單獨實例化的具體服務。 這被稱為服務的生命周期,其余的生命周期選項如下所述。 為您注冊的每個服務選擇適當的生命周期很重要。 應該向請求它的每個類提供一個新的服務實例? 在一個給定的Web請求中應該使用一個實例嗎? 還是應該在應用程序的一生中使用單個實例?

在本文的示例中,有一個簡單的控制器顯示字符名稱,名為CharactersController。 其Index方法顯示當前存儲在應用程序中的字符列表,如果不存在,則使用少數字符初始化集合。 請注意,雖然此應用程序使用Entity Framework CoreApplicationDbContext類作為其持久化,但在控制器中並不明顯。 相反,具體的數據訪問機制已經在遵循倉儲模式的接口ICharacterRepository后面被抽象出來。 通過構造函數請求ICharacterRepository的一個實例,並分配給一個專用字段,然后根據需要使用該字段來訪問字符。

public class CharactersController : Controller
{
    private readonly ICharacterRepository _characterRepository;

    public CharactersController(ICharacterRepository characterRepository)
    {
        _characterRepository = characterRepository;
    }

    // GET: /characters/
    public IActionResult Index()
    {
        PopulateCharactersIfNoneExist();
        var characters = _characterRepository.ListAll();

        return View(characters);
    }
    
    private void PopulateCharactersIfNoneExist()
    {
        if (!_characterRepository.ListAll().Any())
        {
            _characterRepository.Add(new Character("Darth Maul"));
            _characterRepository.Add(new Character("Darth Vader"));
            _characterRepository.Add(new Character("Yoda"));
            _characterRepository.Add(new Character("Mace Windu"));
        }
    }
}

ICharacterRepository定義了控制器需要使用Character實例的兩種方法。

using System.Collections.Generic;
using DependencyInjectionSample.Models;

namespace DependencyInjectionSample.Interfaces
{
    public interface ICharacterRepository
    {
        IEnumerable<Character> ListAll();
        void Add(Character character);
    }
}

該接口由具體類型CharacterRepository實現。

CharacterRepository類一起使用DI的方式是您可以遵循所有應用程序服務的一般模型,而不僅僅是在“倉庫”或數據訪問類中。

using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Models
{
    public class CharacterRepository : ICharacterRepository
    {
        private readonly ApplicationDbContext _dbContext;

        public CharacterRepository(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public IEnumerable<Character> ListAll()
        {
            return _dbContext.Characters.AsEnumerable();
        }

        public void Add(Character character)
        {
            _dbContext.Characters.Add(character);
            _dbContext.SaveChanges();
        }
    }
}

請注意,CharacterRepository在其構造方法中請求一個ApplicationDbContext。 依賴注入以這種鏈式方式使用是不尋常的,每個請求的依賴依次請求自己的依賴關系。 容器負責解析圖中的所有依賴關系,並返回完全解析的服務。

創建請求的對象及其所需的所有對象以及所需的所有對象有時被稱為對象圖。 同樣,必須解決的集合的依賴關系通常被稱為依賴關系樹或依賴圖。

在這種情況下,ICharacterRepositoryApplicationDbContext都必須在啟動中的ConfigureServices中的services容器中注冊。 ApplicationDbContext配置了對擴展方法AddDbContext <T>的調用。 以下代碼顯示了CharacterRepository類型的注冊。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseInMemoryDatabase()
    );

    // Add framework services.
    services.AddMvc();

    // Register application services.
    services.AddScoped<ICharacterRepository, CharacterRepository>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
    services.AddTransient<OperationService, OperationService>();
}

Entity Framework上下文應該使用Scoped(服務在每次請求時被創建,生命周期橫貫整次請求)生命周期添加到服務容器。 如果您使用如上所示的幫助方法,則會自動處理。 Entity Framework的倉儲應該使用相同的生命周期。

注意:
在一個單例中從容器中實例化一個聲明周期為Scoped的服務,在這種情況下,在處理后續請求時,服務可能會處於不正確的狀態。

具有依賴關系的服務應在容器中注冊。 如果服務的構造方法需要一個基元,例如字符串,則可以使用可選參數和配置來注入。

服務的聲明周期和注冊選項

ASP.NET服務可以配置以下生命周期:

Transient

每次請求時創建。 最好用於輕量級無狀態服務。

Scoped

每次請求時創建,貫穿整個請求。

Singleton

Singleton生命周期服務是在第一次請求時創建的(或者當你在指定實例時運行ConfigureServices時),然后每個后續請求都將使用相同的實例。 如果您的應用程序需要單例行為,則允許服務容器管理服務的生命周期,而不是實現單例設計模式,並且自己管理對象的生命周期。

服務可以通過幾種方式向容器注冊。 我們已經看到如何通過指定要使用的具體類型來注冊具有給定類型的服務實現。 此外,還可以指定一個工廠,然后根據需要用於創建實例。 第三種方法是直接指定要使用的類型的實例,在這種情況下,容器將永遠不會嘗試創建一個實例(也不會處理實例)。

為了演示這些生命周期和注冊選項之間的區別,請設計一個簡單的界面,它將一個或多個任務表示為具有唯一標識符OperationId的操作。 根據我們如何配置此服務的生命周期,容器將向請求類提供相同或不同的服務實例。 為了明確要求哪一個生命周期,我們將為每個生命周期創建一個類型選項:

using System;

namespace DependencyInjectionSample.Interfaces
{
    public interface IOperation
    {
        Guid OperationId { get; }
    }

    public interface IOperationTransient : IOperation
    {
    }
    public interface IOperationScoped : IOperation
    {
    }
    public interface IOperationSingleton : IOperation
    {
    }
    public interface IOperationSingletonInstance : IOperation
    {
    }
}

我們使用單個類(即Operation)來實現這些接口,該類在其構造函數中接受Guid,或者如果沒有提供,則使用新的Guid
接下來,在ConfigureServices中,每個類型根據其命名的生命周期添加到容器中:

    services.AddScoped<ICharacterRepository, CharacterRepository>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
    services.AddTransient<OperationService, OperationService>();

請注意,IOperationSingletonInstance服務正在使用具有Guid.Empty的已知ID的特定實例,因此在使用此類型時要清楚(其Guid將全為零)。 我們還注冊了一個取決於每個其他操作類型的OperationService,以便在請求中清楚該服務是否獲得與控制器相同的實例,或者是針對每個操作類型獲得與之相同的實例。 所有這些服務都將其依賴性公開為屬性,因此它們可以顯示在視圖中。

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
    public class OperationService
    {
        public IOperationTransient TransientOperation { get; }
        public IOperationScoped ScopedOperation { get; }
        public IOperationSingleton SingletonOperation { get; }
        public IOperationSingletonInstance SingletonInstanceOperation { get; }

        public OperationService(IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance instanceOperation)
        {
            TransientOperation = transientOperation;
            ScopedOperation = scopedOperation;
            SingletonOperation = singletonOperation;
            SingletonInstanceOperation = instanceOperation;
        }
    }
}

為了演示對應用程序的單獨個別請求內和之間的對象生命周期,示例包括一個OperationsController,它請求每種類型的IOperation類型以及一個OperationService。 'Index'顯示所有控制器和服務的OperationId值。

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionSample.Controllers
{
    public class OperationsController : Controller
    {
        private readonly OperationService _operationService;
        private readonly IOperationTransient _transientOperation;
        private readonly IOperationScoped _scopedOperation;
        private readonly IOperationSingleton _singletonOperation;
        private readonly IOperationSingletonInstance _singletonInstanceOperation;

        public OperationsController(OperationService operationService,
            IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance singletonInstanceOperation)
        {
            _operationService = operationService;
            _transientOperation = transientOperation;
            _scopedOperation = scopedOperation;
            _singletonOperation = singletonOperation;
            _singletonInstanceOperation = singletonInstanceOperation;
        }

        public IActionResult Index()
        {
            // viewbag contains controller-requested services
            ViewBag.Transient = _transientOperation;
            ViewBag.Scoped = _scopedOperation;
            ViewBag.Singleton = _singletonOperation;
            ViewBag.SingletonInstance = _singletonInstanceOperation;
            
            // operation service has its own requested services
            ViewBag.Service = _operationService;
            return View();
        }
    }
}

下圖分別為兩次請求:

觀察在請求中以及請求之間的哪個OperationId值有所不同。

  • Transient 對象總是不同的; 每個控制器和每個服務都提供了一個新的實例。

  • Scoped 對象在請求中是相同的,但在不同的請求中是不同的。

  • Singleton 對象對於每個對象和每個請求都是一樣的(不管ConfigureServices中是否提供一個實例)

請求服務

來自HttpContext的ASP.NET請求中提供的服務通過RequestServices集合公開。

請求服務表示你為應用程序一部分配置和請求的服務。 當您的對象指定依賴關系時,這些都將通過RequestServices中找到的類型而不是ApplicationServices來滿足。

通常,您不應直接使用這些屬性,而是傾向於通過類的構造構造方法請求類所需的類,並讓框架注入這些依賴關系。 這產生了更容易測試的類(參見測試)並且更松散地耦合。

優先要求依賴關系作為訪問RequestServices集合的構造方法參數。

自定義依賴注入服務

你應該設計你的服務以使用依賴注入來獲取他們的協作者。 這意味着避免使用狀態靜態方法調用(這導致一個稱為靜態綁定的代碼)以及服務中依賴類的直接實例化。 當選擇是否實例化一個類型或通過依賴注入來請求它時,這可能有助於記住“New is Glue”這個短語。 通過遵循面向對象設計的SOLID原則,您的類自然會傾向於小型,考慮因素,易於測試。

如果你發現你的類傾向於有太多的依賴關系被注入呢? 這通常是您的類嘗試做的太多的工作,可能違反SRP - 單一職責原則。 看看你是否可以通過將一些責任轉移到一個類中來重構類。 請記住,您的Controller類應該專注於UI問題,因此業務規則和數據訪問實現細節應該保存在適合這些單獨問題的類中。

關於數據訪問,您可以將DbContext注入到控制器中(假設您已將EF添加到ConfigureServices中的服務容器)。 一些開發人員更喜歡使用數據庫的倉儲接口,而不是直接注入DbContext。 使用接口將數據訪問邏輯封裝在一個位置可以最小化數據庫更改時您將需要更改的位置。

釋放服務

容器將為其創建的IDisposable類型調用Dispose。 但是,如果您將自己的實例添加到容器中,則不會被處理。

// Services implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    // container will create the instance(s) of these types and will dispose them
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();

    // container did not create instance so it will NOT dispose it
    services.AddSingleton<Service3>(new Service3());
    services.AddSingleton(new Service3());
}

替換默認服務容器

內置的服務容器旨在滿足框架和在其上生成的大多數使用者應用程序的基本需求。 但是,開發人員可以用其首選容器替換內置容器。 ConfigureServices方法通常返回void,但如果其簽名更改為返回IServiceProvider,則可以配置和返回不同的容器。 有許多IOC容器可用於.NET。 在本示例中,使用Autofac程序包。

首先,安裝相應的程序包:

  • Autofac
  • Autofac.Extensions.DependencyInjection

接下來,在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);
}

使用第三方DI容器時,必須更改ConfigureServices,以使其返回IServiceProvider而不是void

最后,在DefaultModule中配置Autofac

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

在運行時,Autofac將用於解析類型並注入依賴關系。 了解有關使用Autofac和ASP.NET Core的更多信息

Thread safety

單例服務需要線程安全。 如果單例服務依賴於臨時服務,則暫時性服務也可能需要線程安全,取決於單例使用的方式。

建議

在使用依賴注入時,請注意以下建議:

-DI用於具有復雜依賴關系的對象。 控制器,服務,適配器和倉儲都是可能添加到DI的對象的示例。

  • 避免將數據和配置直接存儲在DI中。 例如,用戶的購物車通常不應該添加到服務容器中。 配置應使用選項模型。 同樣,避免只存在的“數據持有者”對象,以允許訪問其他對象。 如果可能,請通過DI請求實際的物品。

  • 避免靜態訪問服務。

  • 避免在應用程序代碼中的服務位置。

  • 避免靜態訪問HttpContext

像所有的建議一樣,你可能會遇到忽略一個需求的情況。 我們發現例外是罕見的 - 在框架本身中大多是非常特殊的情況。

記住,依賴注入是靜態/全局對象訪問模式的替代。 如果將其與靜態對象訪問混合,您將無法實現DI的優點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM