理解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容器中,但是有時我們想要替換或是移除某些服務,這時就需要使用Replace
和Remove
瞭
// 將 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!
推薦閱讀:
- .NET Core使用APB vNext框架入門教程
- .NET中IoC框架Autofac用法講解
- ABP基礎架構深入探索
- ASP.NET Core 依賴註入框架的使用
- .Net Core中使用Autofac替換自帶的DI容器的示例