理解ASP.NET Core 依賴註入(Dependency Injection)

依賴註入

什麼是依賴註入

簡單說,就是將對象的創建和銷毀工作交給DI容器來進行,調用方隻需要接收註入的對象實例即可。

  • 微軟官方文檔-DI

依賴註入有什麼好處

依賴註入在.NET中,可謂是“一等公民”,處處都離不開它,那麼它有什麼好處呢?

假設有一個日志類 FileLogger,用於將日志記錄到本地文件。

public class FileLogger
{
    public void LogInfo(string message)
    {

    }
}

日志很常用,幾乎所有服務都需要記錄日志。如果不使用依賴註入,那麼我們就必須在每個服務中手動 new FileLogger 來創建一個 FileLogger 實例。

public class MyService
{
    private readonly FileLogger _logger = new FileLogger();

    public void Get()
    {
        _logger.LogInfo("MyService.Get");
    }
}

如果某一天,想要替換掉 FileLogger,而是使用 ElkLogger,通過ELK來處理日志,那麼我們就需要將所有服務中的代碼都要改成 new ElkLogger。

public class MyService
{
    private readonly ElkLogger _logger = new ElkLogger();

    public void Get()
    {
        _logger.LogInfo("MyService.Get");
    }
}
  • 在一個大型項目中,這樣的代碼分散在項目各處,涉及到的服務均需要進行修改,顯然一個一個去修改不現實,且違反瞭“開閉原則”。
  • 如果Logger中還需要其他一些依賴項,那麼用到Logger的服務也要為其提供依賴,如果依賴項修改瞭,其他服務也必須要進行更改,更加增大瞭維護難度。
  • 很難進行單元測試,因為它無法進行 mock

正因如此,所以依賴註入解決瞭這些棘手的問題:

  • 通過接口或基類(包含抽象方法或虛方法等)將依賴關系進行抽象化
  • 將依賴關系存放到服務容器中
  • 由框架負責創建和釋放依賴關系的實例,並將實例註入到構造函數、屬性或方法中

ASP.NET Core內置的依賴註入

服務生存周期

Transient
瞬時,即每次獲取,都是一個全新的服務實例

Scoped
范圍(或稱為作用域),即在某個范圍(或作用域內)內,獲取的始終是同一個服務實例,而不同范圍(或作用域)間獲取的是不同的服務實例。對於Web應用,每個請求為一個范圍(或作用域)。

Singleton
單例,即在單個應用中,獲取的始終是同一個服務實例。另外,為瞭保證程序正常運行,要求單例服務必須是線程安全的。

服務釋放

若服務實現瞭IDisposable接口,並且該服務是由DI容器創建的,那麼你不應該去Dispose,DI容器會對服務自動進行釋放。

如,有Service1、Service2、Service3、Service4四個服務,並且都實現瞭IDisposable接口,如:

public class Service1 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service1.Dispose");
    }
}

public class Service2 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service2.Dispose");
    }
}

public class Service3 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service3.Dispose");
    }
}

public class Service4 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service4.Dispose");
    }
}

並註冊為:

public void ConfigureServices(IServiceCollection services)
{
    // 每次使用完(請求結束時)即釋放
    services.AddTransient<Service1>();
    // 超出范圍(請求結束時)則釋放
    services.AddScoped<Service2>();
    // 程序停止時釋放
    services.AddSingleton<Service3>();
    // 程序停止時釋放
    services.AddSingleton(sp => new Service4());
}

構造函數註入一下

public ValuesController(
    Service1 service1, 
    Service2 service2, 
    Service3 service3, 
    Service4 service4)
{ }

請求一下,獲取輸出:

Service2.Dispose
Service1.Dispose

這些服務實例都是由DI容器創建的,所以DI容器也會負責服務實例的釋放和銷毀。註意,單例此時還沒到釋放的時候。

但如果註冊為:

public void ConfigureServices(IServiceCollection services)
{
    // 註意與上面的區別,這個是直接 new 的,而上面是通過 sp => new 的
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());
    services.AddSingleton(new Service3());
    services.AddSingleton(new Service4());
}

此時,實例都是咱們自己創建的,DI容器就不會負責去釋放和銷毀瞭,這些工作都需要我們開發人員自己去做。

更多註冊方式,請參考官方文檔-Service registration methods

TryAdd{Lifetime}擴展方法

當你將同樣的服務註冊瞭多次時,如:

services.AddSingleton<IMyService, MyService>();
services.AddSingleton<IMyService, MyService>();

那麼當使用IEnumerable<{Service}>(下面會講到)解析服務時,就會產生多個MyService實例的副本。

為此,框架提供瞭TryAdd{Lifetime}擴展方法,位於命名空間Microsoft.Extensions.DependencyInjection.Extensions下。當DI容器中已存在指定類型的服務時,則不進行任何操作;反之,則將該服務註入到DI容器中。

services.AddTransient<IMyService, MyService1>();
// 由於上面已經註冊瞭服務類型 IMyService,所以下面的代碼不不會執行任何操作(與生命周期無關)
services.TryAddTransient<IMyService, MyService1>();
services.TryAddTransient<IMyService, MyService2>();
  • TryAdd:通過參數ServiceDescriptor將服務類型、實現類型、生命周期等信息傳入進去
  • TryAddTransient:對應AddTransient
  • TryAddScoped:對應AddScoped
  • TryAddSingleton:對應AddSingleton
  • TryAddEnumerable:這個和TryAdd的區別是,TryAdd僅根據服務類型來判斷是否要進行註冊,而TryAddEnumerable則是根據服務類型和實現類型一同進行判斷是否要進行註冊,常常用於註冊同一服務類型的多個不同實現。舉個例子吧:
// 註冊瞭 IMyService - MyService1
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
// 註冊瞭 IMyService - MyService2
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService2>());
// 未進行任何操作,因為 IMyService - MyService1 在上面已經註冊瞭
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());

解析同一服務的多個不同實現

默認情況下,如果註入瞭同一個服務的多個不同實現,那麼當進行服務解析時,會以最後一個註入的為準。

如果想要解析出同一服務類型的所有服務實例,那麼可以通過IEnumerable<{Service}>來解析(順序同註冊順序一致):

public interface IAnimalService { }
public class DogService : IAnimalService { }
public class PigService : IAnimalService { }
public class CatService : IAnimalService { }

public void ConfigureServices(IServiceCollection services)
{
    // 生命周期沒有限制
    services.AddTransient<IAnimalService, DogService>();
    services.AddScoped<IAnimalService, PigService>();
    services.AddSingleton<IAnimalService, CatService>();
}

public ValuesController(
    // CatService
    IAnimalService animalService,   
    // DogService、PigService、CatService
    IEnumerable<IAnimalService> animalServices)
{
}

Replace && Remove 擴展方法

上面我們所提到的,都是註冊新的服務到DI容器中,但是有時我們想要替換或是移除某些服務,這時就需要使用ReplaceRemove

// 將 IMyService 的實現替換為 MyService1
services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 註冊的實現 MyService
services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 的所有註冊
services.RemoveAll<IMyService>();
// 清除所有服務註冊
services.Clear();

Autofac

Autofac 是一個老牌DI組件瞭,接下來我們使用Autofac替換ASP.NET Core自帶的DI容器。

1.安裝nuget包:

Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection

2.替換服務提供器工廠

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        // 通過此處將默認服務提供器工廠替換為 autofac
        .UseServiceProviderFactory(new AutofacServiceProviderFactory());

3.在 Startup 類中添加 ConfigureContainer 方法

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public ILifetimeScope AutofacContainer { get; private set; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 1. 不要 build 或返回任何 IServiceProvider,否則會導致 ConfigureContainer 方法不被調用。
        // 2. 不要創建 ContainerBuilder,也不要調用 builder.Populate(),AutofacServiceProviderFactory 已經做瞭這些工作瞭
        // 3. 你仍然可以在此處通過微軟默認的方式進行服務註冊
        
        services.AddOptions();
        services.AddControllers();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication.Ex", Version = "v1" });
        });
    }

    // 1. ConfigureContainer 用於使用 Autofac 進行服務註冊
    // 2. 該方法在 ConfigureServices 之後運行,所以這裡的註冊會覆蓋之前的註冊
    // 3. 不要 build 容器,不要調用 builder.Populate(),AutofacServiceProviderFactory 已經做瞭這些工作瞭
    public void ConfigureContainer(ContainerBuilder builder)
    {
        // 將服務註冊劃分為模塊,進行註冊
        builder.RegisterModule(new AutofacModule());
    }
    
    public class AutofacModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            // 在此處進行服務註冊
            builder.RegisterType<UserService>().As<IUserService>();
        }
    }

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {
        // 通過此方法獲取 autofac 的 DI容器
        AutofacContainer = app.ApplicationServices.GetAutofacRoot();
    }
}

服務解析和註入

上面我們主要講瞭服務的註入方式,接下來看看服務的解析方式。解析方式有兩種:

1.IServiceProvider

2.ActivatorUtilities

  • 用於創建未在DI容器中註冊的服務實例
  • 用於某些框架級別的功能

構造函數註入

上面我們舉得很多例子都是使用瞭構造函數註入——通過構造函數接收參數。構造函數註入是非常常見的服務註入方式,也是首選方式,這要求:

  • 構造函數可以接收非依賴註入的參數,但必須提供默認值
  • 當服務通過IServiceProvider解析時,要求構造函數必須是public
  • 當服務通過ActivatorUtilities解析時,要求構造函數必須是public,雖然支持構造函數重載,但必須隻能有一個是有效的,即參數能夠全部通過依賴註入得到值

方法註入

顧名思義,方法註入就是通過方法參數來接收服務實例。

[HttpGet]
public string Get([FromServices]IMyService myService)
{
    return "Ok";
}

屬性註入

ASP.NET Core內置的依賴註入是不支持屬性註入的。但是Autofac支持,用法如下:

老規矩,先定義服務和實現

public interface IUserService 
{
    string Get();
}

public class UserService : IUserService
{
    public string Get()
    {
        return "User";
    }
}

然後註冊服務

  • 默認情況下,控制器的構造函數參數由DI容器來管理嗎,而控制器實例本身卻是由ASP.NET Core框架來管理,所以這樣“屬性註入”是無法生效的
  • 通過AddControllersAsServices方法,將控制器交給 autofac 容器來處理,這樣就可以使“屬性註入”生效瞭
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddControllersAsServices();
}

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterModule<AutofacModule>();
}

public class AutofacModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<UserService>().As<IUserService>();

        var controllerTypes = Assembly.GetExecutingAssembly().GetExportedTypes()
            .Where(type => typeof(ControllerBase).IsAssignableFrom(type))
            .ToArray();

        // 配置所有控制器均支持屬性註入
        builder.RegisterTypes(controllerTypes).PropertiesAutowired();
    }
}

最後,我們在控制器中通過屬性來接收服務實例

public class ValuesController : ControllerBase
{
    public IUserService UserService { get; set; }

    [HttpGet]
    public string Get()
    {
        return UserService.Get();
    }
}

通過調用Get接口,我們就可以得到IUserService的實例,從而得到響應

User

一些註意事項

  • 避免使用服務定位模式。盡量避免使用GetService來獲取服務實例,而應該使用DI。
using Microsoft.Extensions.DependencyInjection;

public class ValuesController : ControllerBase
{
    private readonly IServiceProvider _serviceProvider;

    // 應通過依賴註入的方式獲取服務實例
    public ValuesController(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    [HttpGet]
    public string Get()
    {
        // 盡量避免通過 GetService 方法獲取服務實例
        var myService = _serviceProvider.GetService<IMyService>();

        return "Ok";
    }
}
  • 避免在ConfigureServices中調用BuildServiceProvider。因為這會導致創建第二個DI容器的副本,從而導致註冊的單例服務出現多個副本。
public void ConfigureServices(IServiceCollection services)
{
    // 不要在該方法中調用該方法
    var serviceProvider = services.BuildServiceProvider();
}
  • 一定要註意服務解析范圍,不要在 Singleton 中解析 Transient 或 Scoped 服務,這可能導致服務狀態錯誤(如導致服務實例生命周期提升為單例)。允許的方式有:

1.在 Scoped 或 Transient 服務中解析 Singleton 服務

2.在 Scoped 或 Transient 服務中解析 Scoped 服務(不能和前面的Scoped服務相同)

  • 當在Development環境中運行、並通過 CreateDefaultBuilder生成主機時,默認的服務提供程序會進行如下檢查:

1.不能在根服務提供程序解析 Scoped 服務,這會導致 Scoped 服務的生命周期提升為 Singleton,因為根容器在應用關閉時才會釋放。

2.不能將 Scoped 服務註入到 Singleton 服務中

  • 隨著業務增長,需要依賴註入的服務也越來越多,建議使用擴展方法,封裝服務註入,命名為Add{Group_Name},如將所有 AppService 的服務註冊封裝起來
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ApplicationServiceCollectionExtensions
    {
        public static IServiceCollection AddApplicationService(this IServiceCollection services)
        {
            services.AddTransient<Service1>();
            services.AddScoped<Service2>();
            services.AddSingleton<Service3>();
            services.AddSingleton(sp => new Service4());

            return services;
        }
    }
}

然後在ConfigureServices中調用即可

public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationService();
}

框架默認提供的服務

以下列出一些常用的框架已經默認註冊的服務:

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

到此這篇關於理解ASP.NET Core 依賴註入(Dependency Injection)的文章就介紹到這瞭,更多相關ASP.NET Core 依賴註入內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: