注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
依賴注入
什麼是依賴注入
簡單說,就是將物件的建立和銷燬工作交給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容器。
- 安裝nuget包:
Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection
- 替換服務提供器工廠
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
// 通過此處將預設服務提供器工廠替換為 autofac
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
- 在 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();
}
}
服務解析和注入
上面我們主要講了服務的注入方式,接下來看看服務的解析方式。解析方式有兩種:
- IServiceProvider
- 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 服務,這可能導致服務狀態錯誤(如導致服務例項生命週期提升為單例)。允許的方式有:
- 在 Scoped 或 Transient 服務中解析 Singleton 服務
- 在 Scoped 或 Transient 服務中解析 Scoped 服務(不能和前面的Scoped服務相同)
-
當在
Development
環境中執行、並通過CreateDefaultBuilder
生成主機時,預設的服務提供程式會進行如下檢查:- 不能在根服務提供程式解析 Scoped 服務,這會導致 Scoped 服務的生命週期提升為 Singleton,因為根容器在應用關閉時才會釋放。
- 不能將 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 |