爲什麼需要依賴注入框架
- 藉助依賴注入框架,可以輕鬆管理類之間的依賴,幫助我們在構建應用時遵循設計原則,確保代碼的可維護性和可擴展性。
- ASP.NET Core的整個架構中,依賴注入框架提供了對象創建和生命週期管理的核心能力,各個組件相互協作,也是依靠依賴注入框架的能力來實現的。
組件包
它採用接口實現分離模式,其中Microsoft.Extensions.DependencyInjection.Abstractions
是抽象包,Microsoft.Extensions.DependencyInjection
是具體實現。
意味着後續我們可以用第三方包來替代默認實現。
核心類型
IServiceCollection
,負責服務的註冊。ServiceDescriptor
,每一個服務註冊時的信息。IServiceProvider
,具體的容器,也是由ServiceCollection
Build出來的。IServiceScope
,表示一個容器的子容器的生命週期。
生命週期(Sevice Lifetime)
單例(Singleton)
,指在整個根容器的生命週期內,都是單例,不管你是根容器還是子容器。作用域(Scoped)
,在容器的生存週期內,或者子容器的生存週期內,如果我的容器釋放掉,意味着我的對象也會釋放掉,在這個範圍內我們得到的是個單例模式。瞬時(暫時)Transient
,指我們每一次從容器中獲取對象時,都可以得到一個全新的對象。
單例和作用域的區別是,單例是全局的單例,作用域是範圍內的單例。
實踐理解
創建項目
dotnet new sln -o HelloDependencyInjection
cd .\HelloDependencyInjection\
dotnet new webapi -o demoForDI60 -f net6.0
dotnet sln add .\demoForDI60\demoForDI60.csproj
dotnet new webapi -o demoForDI31 -f netcoreapp3.1
dotnet sln add .\demoForDI31\demoForDI31.csproj
code .
explorer.exe .
準備一些代碼
public interface IMyScopedService { }
public class MyScopedService : IMyScopedService
{
}
public interface IMySingletonService { }
public class MySingletonService : IMySingletonService
{
}
public interface IMyTransientService { }
public class MyTransientService : IMyTransientService
{
}
註冊不同服務
// demoForDI31\Startup.cs
public void ConfigureServices(IServiceCollection services)
{
#region 註冊服務不同生命週期的服務
services.AddSingleton<IMySingletonService, MySingletonService>();
services.AddScoped<IMyScopedService, MyScopedService>();
services.AddTransient<IMyTransientService, MyTransientService>();
#endregion
services.AddControllers();
}
這裏將IMySingletonService
組註冊成單例模式,將IMyScopedService
組註冊成作用域模式,將IMyTransientService
組註冊成瞬時模式。
然後在WeatherForecastController.cs
中添加一個檢驗的方法,新增一個GetServices
接口來打印從容器中讀取的六個實例的HashCode值。
[HttpGet("GetServices")]
public int GetServices
(
[FromServices] IMySingletonService singleton1,
[FromServices] IMySingletonService singleton2,
[FromServices] IMyScopedService scoped1,
[FromServices] IMyScopedService scoped2,
[FromServices] IMyTransientService transient1,
[FromServices] IMyTransientService transient2
)
{
Console.WriteLine($"請求開始");
Console.WriteLine($"singleton1:{singleton1.GetHashCode()}");
Console.WriteLine($"singleton2:{singleton2.GetHashCode()}");
Console.WriteLine($"scoped1:{scoped1.GetHashCode()}");
Console.WriteLine($"scoped2:{scoped2.GetHashCode()}");
Console.WriteLine($"transient1:{transient1.GetHashCode()}");
Console.WriteLine($"transient2:{transient2.GetHashCode()}");
Console.WriteLine($"請求結束");
return 1;
}
這裏使用了[FromServices]
代表從容器中獲取實例。
分析結果
將demoForDI31\Properties\launchSettings.json
中demoForDI31
節點下的launchUrl
改成:weatherforecast/GetServices
。
運行後,我們看下結果。
請求開始
singleton1:687191
singleton2:687191
scoped1:49385318
scoped2:49385318
transient1:13062350
transient2:10366524
請求結束
在運行一次,看下結果
請求開始
singleton1:687191
singleton2:687191
scoped1:26847985
scoped2:26847985
transient1:199777
transient2:8990007
請求結束
從這兩組數據可以看出,註冊爲單例的實例在兩次運行中HashCode是相同的,兩個瞬時實例得到的HashCode是完全不同的。對作用域實例來說,單次請求得到的HashCode是相同的,但是兩次請求的值是不同的。
花式註冊
1. 直接注入實例
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, TService implementationInstance) where TService : class;
services.AddSingleton<IOrderService>(new OrderService());
2. 工程方式註冊
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
services.AddSingleton<IOrderService>(serviceProvider =>
{
return new OrderService();
});
services.AddScoped<IOrderService>(serviceProvider =>
{
return new OrderService();
});
services.AddTransient<IOrderService>(serviceProvider =>
{
return new OrderService();
});
3. 嘗試註冊(接口維度)
嘗試註冊意思是,註冊的接口對應的實現已經註冊過了,就不重複註冊了。
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddSingleton<IOrderService, OrderServiceEx>();
然後我們新增一個GetServiceList
路由節點來打印從容器中獲取到的IOrderService
到底有多少個。
[HttpGet("GetServiceList")]
public int GetServiceList([FromServices] IEnumerable<IOrderService> orderServices)
{
foreach (var orderService in orderServices)
{
Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
}
return 1;
}
執行結果來看。
demoForDI31.Services.OrderService:38196344
只有一個,說明第二次試圖再次註冊IOrderService
失敗了。
如果註冊兩個實例會怎麼樣?
services.AddSingleton<IOrderService>(new OrderService());
services.AddSingleton<IOrderService, OrderServiceEx>();
這時候輸出是
demoForDI31.Services.OrderService:31523018
demoForDI31.Services.OrderServiceEx:17375337
那麼當IOrderService
存在兩個註冊實例的時候,默認會取哪一個呢?
[HttpGet("GetService")]
public int GetService([FromServices]IOrderService orderService)
{
Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
return 1;
}
運行結果是:
demoForDI31.Services.OrderServiceEx:50833863
我們會看到,它默認會取最後註冊的那一個,也就是說,同一個服務註冊多次,如果取服務實例的時候,默認取的是最後一個。
4. 嘗試註冊(實現維度)
這裏有個特殊的用法,就是通過TryAddEnumerable
方法可以實現,實現不同就可以註冊進去,如果實現相同就不註冊進去。
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService>());
得到的結果只有一個,因爲後面註冊的接口實現類和之前一樣,所以註冊不進去。
demoForDI31.Services.OrderService:24250448
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());
得到的結果是
demoForDI31.Services.OrderService:24250448
demoForDI31.Services.OrderServiceEx:40098280
這時候發現,OrderService
和OrderServiceEx
實現不一樣,所以兩個成功註冊進去了。
這樣能解決的那種,同一個接口,不希望被註冊多次,但是不同實現可以被註冊的場景。
替換註冊
通過Replace
可以替換掉之前註冊的服務爲新註冊的服務。
services.AddSingleton<IOrderService>(new OrderService());
services.Replace(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());
運行結果
demoForDI31.Services.OrderServiceEx:24250448
可以看到前面註冊的OrderService
被替換成OrderServiceEx
實例了。
移除註冊
services.RemoveAll<IOrderService>();
運行結果
實際爲空,也就是沒有註冊實例可打印了。
註冊泛型模板
準備泛型模板代碼
public interface IGenericService<T> { }
public class GenericService<T> : IGenericService<T>
{
public T Data { get; private set; }
public GenericService(T data)
{
this.Data = data;
}
}
註冊泛型模板
services.AddSingleton(typeof(IGenericService<>), typeof(GenericService<>));
需要用typeof
來註冊。
使用泛型模板
public WeatherForecastController(ILogger<WeatherForecastController> logger, IGenericService<IOrderService> genericService)
{
_logger = logger;
}
運行之後,我們可以看到genericService
的Data是demoForDI31.Services.OrderService
。
依賴注入實例的兩種方法
1. 通過構造函數方式
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOrderService orderService)
{
_logger = logger;
}
適合場景:當我們定義一個Controller的時候,它的服務是大部分接口都需要使用的情況下,推薦從構造函數方式來注入。
2. 使用FromServices方式
[HttpGet("GetService")]
public int GetService([FromServices]IOrderService orderService)
{
Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
return 1;
}
適合場景:當我們一個服務僅僅是在某一個接口場景下使用,推薦使用FromServices的方式來注入。
實現IDisposable接口類型的釋放
基本原則
- DependencyInjection只負責釋放由其創建的對象實例。
- DependencyInjection在容器或者子容器釋放時,纔會去釋放由其創建的對象實例。
建議
- 避免在根容器創建實現了IDisposable接口的瞬時服務。
- 避免手動創建對象然後塞到容器裏面,而應該儘量使用容器來管理對象的創建和釋放。
實踐理解
準備代碼
public interface IOrderService { }
public class DisposableOrderService : IOrderService, IDisposable
{
public void Dispose()
{
Console.WriteLine($"DisposableOrderService Disposed:{this.GetHashCode()}");
}
}
驗證釋放(瞬時模式)
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOrderService, DisposableOrderService>();
services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
Console.WriteLine($"接口請求處理結束");
return 1;
}
執行結果是
接口請求處理結束
DisposableOrderService Disposed:15401461
DisposableOrderService Disposed:25712189
可見這兩個實例對象是在整個請求結束之後,纔會去觸發釋放。
但是仍然有一種情況很危險,那就是實現了IDisposable
的瞬時服務,如果是在根容器去獲取和創建,那麼它會一直保持到整個程序生命週期結束纔會被釋放。
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOrderService, DisposableOrderService>();
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.ApplicationServices.GetService<IOrderService>();
}
這裏定義了瞬時的IOrderService
服務,但是接下來在根容器中獲取並創建了它。
[HttpGet]
public int Get
(
[FromServices] IHostApplicationLifetime hostApplicationLifetime,
[FromQuery] bool stop = false
)
{
Console.WriteLine($"接口請求處理結束");
if (stop)
{
hostApplicationLifetime.StopApplication();
}
return 1;
}
正常運行結果:
接口請求處理結束
沒有之前的釋放信息。
傳入關閉動作:
接口請求處理結束
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
DisposableOrderService Disposed:20074041
這時候這個瞬時服務實例才被真正釋放掉。
驗證釋放(作用域模式)
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IOrderService, DisposableOrderService>();
services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
Console.WriteLine($"子容器開始");
using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
{
var service1 = scope.ServiceProvider.GetService<IOrderService>();
var service2 = scope.ServiceProvider.GetService<IOrderService>();
}
Console.WriteLine($"子容器結束");
Console.WriteLine($"接口請求處理結束");
return 1;
}
我們來看看結果
子容器開始
DisposableOrderService Disposed:20281500
子容器結束
接口請求處理結束
DisposableOrderService Disposed:54516368
我們發現,子容器中那個作用域對象是在子容器生命週期結束時就釋放了,而外面那個作用域是在整個請求結束後釋放的。
驗證釋放(單例模式)
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IOrderService, DisposableOrderService>();
services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
Console.WriteLine($"接口請求處理結束");
return 1;
}
接口請求處理結束
可以看到整個接口請求結束之後,這個單例的實例還沒有被釋放,那麼單例實例會在什麼時候釋放呢?
[HttpGet]
public int Get
(
[FromServices] IOrderService orderService1,
[FromServices] IOrderService orderService2,
[FromServices] IHostApplicationLifetime hostApplicationLifetime,
[FromQuery] bool stop = false
)
{
Console.WriteLine($"接口請求處理結束");
if (stop)
{
hostApplicationLifetime.StopApplication();
}
return 1;
}
接口請求處理結束
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
DisposableOrderService Disposed:48950176
這裏我們看到,當使用IHostApplicationLifetime
接口中StopApplication
方法,強行停止整個程序的時候,這時候這個單例實例終於被釋放了。
但是單例模式下,如果我們手動創建實例對象
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IOrderService>(new DisposableOrderService());
services.AddControllers();
}
即使是手動關閉程序,你會發現結果是
接口請求處理結束
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
這種手動創建的單例服務對象,並不會在根容器生命週期結束的時候被釋放。