asp.net core 系列之Dependency injection(依賴注入)

這篇文章主要講解asp.net core 依賴注入的一些內容。html

ASP.NET Core支持依賴注入。這是一種在類和其依賴之間實現控制反轉的一種技術(IOC).web

一.依賴注入概述

1.原始的代碼數據庫

依賴就是一個對象的建立須要另外一個對象。下面的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類的依賴(IndexModel類的建立須要用到MyDependency)api

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

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

2.原始代碼分析安全

IndexModel類建立了MyDependency類,而且直接依賴MyDependency實例。上面的代碼依賴是有問題的,而且應該被避免(避免直接建立依賴的實例對象),服務器

緣由以下:框架

  • 須要用一個不一樣的實現來替換MyDependency,這個類必須被修改
  • 若是MyDependency有依賴,他們必須被這個類配置。在一個有不少類依賴MyDependency的大的項目中,配置代碼在應用中會很分散。
  • 這種實現對於單元測試是困難的。對於MyDependency,應用應該使用mock或者stub,用這種方式是不可能的。

依賴注入解決那些問題asp.net

  • 接口的使用抽象了依賴的實現
  • service container註冊依賴。ASP.NET Core提供了一個內置的service container, IServiceProvider.  Services是在應用的Startup.ConfigureServices中被註冊。
  • 一個類是在構造函數中注入service。框架執行着建立一個帶依賴的實例的責任,而且當不須要時,釋放。

3.下面是改良後的代碼less

這示例應用中,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);
    }
}

在示例中,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.");
    }
}

 

4.改良代碼分析及擴展講解(使用DI)

 MyDependency在構造函數中,要求有一個ILogger<TCategoryName>。用一種鏈式的方法使用依賴注入是很常見的每一個依賴依次再請求它本身須要的依賴。(即:MyDependency是一個依賴,同時,建立MyDependency又須要其餘依賴:ILogger<TCategoryName>。)

IMyDependency和ILogger<TCategoryName>必須在service container中註冊。IMyDependency是在Startup.ConfigureServices中註冊。ILogger<TCategoryName>是被logging abstractions infrastructure註冊,因此它是一種默認已經註冊的框架提供的服務。(即框架自帶的已經註冊的服務,不須要再另外註冊)

容器解析ILogger<TCategoryName>,經過利用泛型. 消除註冊每一種具體的構造類型的須要。(由於在上面的例子中,ILogger中的泛型類型爲MyDependency,可是若是在其餘類中使用ILogger<>, 類型則是其餘類型,這裏使用泛型比較方便)

services.AddSingleton(typeof(ILogger<T>), typeof(Logger<T>)); 

這是它的註冊的語句(框架實現的),其中的用到泛型,而不是一種具體的類型。

在示例應用中,IMyDependency service是用具體的類型MyDependency來註冊的。這個註冊包括服務的生命週期(service lifetime)。Service lifetimes隨後會講。

若是服務的構造函數要求一個內置類型,像string,這個類型能夠被使用configuration 或者options pattern來注入

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

或者 options pattern(注意:不止這些,這裏簡單舉例)

二.框架提供的服務(Framework-provided services)

 Startup.ConfigureServices方法有責任定義應用使用的服務,包括平臺功能,例如Entity Framework CoreASP.NET Core MVC。最初,IServiceColletion提供給ConfigureServices下面已經定義的服務(依賴於怎樣配置host):

 

當一個service colletion 擴展方法能夠用來註冊一個服務,習慣是用一個單獨的Add{SERVICE_NAME} 擴展方法來註冊服務所須要的全部服務。下面的代碼是一個怎麼使用擴展方法AddDbContext, AddIdentity,和AddMvc, 添加額外的服務到container:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

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

    services.AddMvc();
}

更多的信息:ServiceCollection Class 

三. 服務生命週期(Service lifetimes)

爲每一個註冊的服務選擇一個合適的生命週期。ASP.NET Core服務能夠用下面的聲明週期配置:

TransientScopedSingleton

Transient(臨時的)

臨時的生命週期服務是在每次從服務容器中被請求時被建立。這個生命週期對於lightweight(輕量的),stateless(無狀態的)服務比較合適。

Scoped(範圍)

範圍生命週期被建立,一旦每一個客戶端請求時(connection)

警告:當在中間件中使用範圍服務時,注入服務到Invoke或者InvokeAsync方法。不要經過構造函數注入,由於那回強制服務表現的像是singleton(單例)

Singleton(單獨)

單獨生命週期在第一次請求時被建立(或者說當ConfigureService運行而且被service registration指定時)。以後每個請求都使用同一個實例。若是應用要求一個單獨行爲(singleton behavior),容許service container來管理服務生命週期是被推薦的。不要實現一個單例設計模式而且在類中提供用戶代碼來管理這個對象的生命週期。

警告:從一個singleton來解析一個範圍服務(scoped service)是危險的。它可能會形成服務有不正確的狀態,當處理隨後的請求時。

構造函數注入行爲

服務能夠被經過兩種機制解析:

  • IServiceProvider
  • ActivatorUtilities : 容許對象建立,能夠不經過在依賴注入容器中注入的方式。ActivatorUtilities是使用user-facing abstractions,例如Tag Helpers , MVC controllers 和 model binders.

構造函數能夠接受參數,不經過依賴注入提供,可是這些參數必須指定默認值。

當服務被經過IServiceProvider或者ActivatorUtilities解析時,構造函數注入要求一個公共的構造函數。

當服務被ActivatorUtilities解析時,構造函數注入要求一個合適的構造函數存在。構造函數的重載是被支持的,可是隻有一個重載能夠存在,它的參數能夠被依賴注入執行(即:能夠被依賴注入執行的,只有一個構造函數的重載)。

四. Entity Framework contexts

Entity Framework contexts 一般使用scoped lifetime ,添加到服務容器中(service container).由於web 應用數據庫操做的範圍適用於client request(客戶端請求)。默認的生命週期是scoped,若是一個生命週期沒有被AddDbContext<TContext>重載指定,當註冊database context時。給出生命週期的服務不該該使用一個生命週期比服務的生命週期短的database context.

五.Lifetime and registration options

 爲了說明lifetimeregistration options之間的不一樣,考慮下面的接口:這些接口表示的任務都是帶有惟一標識的操做。取決於這些接口的操做服務的生命週期怎麼配置,container提供了要麼是同一個要麼是不一樣的服務當被一個類請求時:

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,若是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被經過依賴注入請求,它要麼接收每一個服務的一個新實例要麼接收一個已經存在的實例(在依賴服務的生命週期的基礎上)

  • 當臨時服務(transient services)被建立時,當被從容器中請求時,IOperationTransient服務的OperationId是不一樣的。OperationService接收到一個IOperationTransient類的實例。這個新實例產生一個不一樣的OperationId.
  • 每一個client請求時,scoped services被建立,IOperationScoped serviceOperationId是同樣的,在一個client request內。跨越client requests,兩個service享用一個不一樣的OperationId的值。
  • singletonsingleton-instance服務一旦被建立,而且被使用跨越全部的client requests和全部的服務,則OperationId跨越全部的service requests是一致的。
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.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服務是一個特殊的實例,它的IDGuid.Empty. 它是清楚的,當這個類型被使用(它的GUID都是0組成的)

示例應用說明了requests內的對象生命週期和兩個requests之間的對象生命週期。示例應用的IndexModel請求IOperation的每一個類型和OperationService。這個頁面展現了全部的這個page model類的和服務的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.");
    }
}

下面的輸出展現了兩個請求的結果:

從結果看出:

  • Transient對象老是不一樣的。Transient OperationId的值對於第一個和第二個客戶端請求是在OperationService中不一樣的,而且跨越client requests. 一個新的實例被提供給每一個service requestclient request.
  • Scoped對象對於一個client request內部是同樣的,跨越client request是不一樣的。
  • Singleton對象對於每一個對象和每一個請求都是同樣的,無論Operation實例是否在ConfigureServices中被提供了。

 能夠看出,Transient一直在變;Scoped 同一個client request請求內不變;Singleton一直不變;

 六. Call Services from main(在main中調用services

 IServiceScopeFactory.CreateScope建立一個IServiceScope 來解析一個scoped service在應用的範圍內。這個方式是有用的對於在Startup中獲得一個scoped service 來運行初始化任務。下面的例子展現了MyScopedServcie怎樣包含一個context,在Program.Main中:

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

七.Scope validation(範圍驗證)

 當應用在開發環境運行時,默認的service provider 執行檢查來驗證:

  • Scoped services不是直接或間接的被從root service provider中解析
  • Scoped services 不是直接或間接的被注入爲singletons

 root service provider 是當BuildServiceProvider被調用時被建立的。Root service provider的生命週期對應於應用/服務器 的生命週期,當provider隨着應用啓動而且當應用關閉時會被釋放。

Scoped服務被建立它們的容器釋放。若是scoped serviceroot container中被建立,服務的生命週期其實是被提高爲singleton,由於它只有當應用或者服務器關閉時纔會被root container釋放。驗證servcie scopes 注意這些場景,當BuildServiceProvider被調用時。

八.Request Services

來自HttpContextASP.NET Core request中的可用的services經過HttpContext.RequestServices集合來暴露。

Request Services表明應用中被配置的services和被請求的部分。當對象指定依賴,會被RequestService中的類型知足,而不是ApplicationServices中的。

一般,應用不該該直接使用那些屬性。相反的,請求知足那個類型的的這些類,能夠經過構造函數而且容許框架注入這些依賴。這使類更容易測試。

注意:請求依賴,經過構造函數參數來獲得RequestServices集合更受歡迎。

九. Design services for dependency injection

 最佳實踐:

  • 設計services使用依賴注入來包含它們的依賴
  • 避免stateful,靜態的方法調用
  • 避免在services內直接初始化依賴類。直接初始化是代碼關聯一個特定的實現
  • 使應用的類small, well-factored,easily tested.

 若是一個相似乎有不少注入的依賴,這一般是它有太多職責的信號,而且違反了Single Responsibility Principle(SRP)單一職責原則。嘗試經過移動一些職責到一個新類來重構這個類。記住,Razor Pages page model classesMVC controller classes應該專一於UI層面。Business rulesdata access implementation細節應該在那些合適的分開的關係的類中。

Disposal of services

 容器爲它建立的類調用IDisposableDispose。若是一個實例被用戶代碼添加到容器中,它不會自動釋放。

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

即,若是,類是被用戶代碼添加容器中的,不會自動釋放。像下面這種直接new類的。

十.Default service container replacement

內置的service container意味着提供服務來知足框架和大多消費應用的需求。咱們建議使用功能內置容器,除非你須要一個特殊的功能,內置容器不支持。有些功能在第三方容器支持,可是內置容器不支持:

  • Property injection
  • Injection based on name
  • Child containers
  • Custom lifetime management
  • Fun<T> support for lazy initializtion

下面的示例,使用Autofac替代內置容器:

  • 安裝合適的容器包:
    • Autofac
    • Autofac.Extensions.DependencyInjection
  • 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 documentation.

Thread safety

建立線程安全的單例服務。若是一個單例服務對一個臨時的服務有依賴,這個臨時的服務可能須要要求線程安全根據它怎樣被單例服務使用。

單例服務的工廠方法,例如AddSingleton<TService>(IServiceColletion, Func<IServiceProvider, TService>)的第二個參數,不須要線程安全。像一個類型的構造函數,它一次只能被一個線程調用。

十一.Recommendations

  • Async/await Task 依據service resolution(服務解決)是不支持的。C# 不支持異步的構造函數;所以,推薦的模式是在同步解析服務以後使用異步方法。
  • 避免直接在service container中存儲數據和配置。例如,用戶的購物車不該該被添加到service container. 配置應該使用option pattern. 類似的,避免data holder對象可接近其餘對象。最好是請求實際的item經過DI.
  • 避免靜態獲得services(例如,靜態類型IApplicationBuilder.ApplicationServices的在別處的使用)
  • 避免使用service locator pattern. 例如,當你能夠用DI時,不要用GetService來獲取一個服務。

錯誤的:

public void MyMethod()
{
    var options = 
        _services.GetService<IOptionsMonitor<MyOptions>>();
    var option = options.CurrentValue.Option;

    ...
}

正確的:

private readonly MyOptions _options;

public MyClass(IOptionsMonitor<MyOptions> options)
{
    _options = options.CurrentValue;
}

public void MyMethod()
{
    var option = _options.Option;

    ...
}
  • 另外一個service locator 變量要避免,是注入一個在運行時解析依賴的工廠。那些實踐的二者都混合了Inversion of Control策略(即避免依賴注入和其餘方式混合使用)。
  • 避免靜態獲得HttpContext(例如,IHttpContextAccessor.HttpContext

有時候的場景,可能須要忽略其中的建議。

DIstatic/global object access patterns的可替代方式。若是你把它和static object access 方式混合使用,可能不能認識到DI的好處。

參考網址:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2

相關文章
相關標籤/搜索