乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 貫穿ASP.NET Core整個架構的依賴注入框架(Dependency Injection)

爲什麼需要依賴注入框架

image

  • 藉助依賴注入框架,可以輕鬆管理類之間的依賴,幫助我們在構建應用時遵循設計原則,確保代碼的可維護性和可擴展性。
  • ASP.NET Core的整個架構中,依賴注入框架提供了對象創建和生命週期管理的核心能力,各個組件相互協作,也是依靠依賴注入框架的能力來實現的。

組件包

它採用接口實現分離模式,其中Microsoft.Extensions.DependencyInjection.Abstractions是抽象包,Microsoft.Extensions.DependencyInjection是具體實現。

意味着後續我們可以用第三方包來替代默認實現。

核心類型

  • IServiceCollection,負責服務的註冊。
  • ServiceDescriptor,每一個服務註冊時的信息。
  • IServiceProvider,具體的容器,也是由ServiceCollection Build出來的。
  • IServiceScope,表示一個容器的子容器的生命週期。

生命週期(Sevice Lifetime)

  • 單例(Singleton),指在整個根容器的生命週期內,都是單例,不管你是根容器還是子容器。
  • 作用域(Scoped),在容器的生存週期內,或者子容器的生存週期內,如果我的容器釋放掉,意味着我的對象也會釋放掉,在這個範圍內我們得到的是個單例模式。
  • 瞬時(暫時)Transient,指我們每一次從容器中獲取對象時,都可以得到一個全新的對象。

單例和作用域的區別是,單例是全局的單例,作用域是範圍內的單例。

實踐理解

https://github.com/TaylorShi/HelloDependencyInjection

創建項目

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 .

image

準備一些代碼

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.jsondemoForDI31節點下的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

這時候發現,OrderServiceOrderServiceEx實現不一樣,所以兩個成功註冊進去了。

這樣能解決的那種,同一個接口,不希望被註冊多次,但是不同實現可以被註冊的場景。

替換註冊

通過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

image

依賴注入實例的兩種方法

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...

這種手動創建的單例服務對象,並不會在根容器生命週期結束的時候被釋放。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章