1. 程式人生 > 實用技巧 >依賴注入簡介

依賴注入簡介

為什麼要使用依賴注入框架(Dependence Injection)

  • 藉助依賴注入框架,我們可以輕鬆管理類之間的依賴,幫助我們在構建應用時遵循原則,確保程式碼的可維護性和可擴充套件性。

  • ASP.NET Core的整個架構中,依賴注入框架提供了物件建立和生命週期管理的核心能力,各個元件相互協作,並實現控制反轉(IOC)。

元件包:

體現了一個經典的設計模式:介面實現分離模式

以為著我們的元件只需要依賴抽象的介面,當使用時注入它的具體實現即可,意味著我們可以在未來做任意的擴充套件。

  • Microsoft.Extensions.DependencyInjection.Abstractions
  • Microsoft.Extensions.DependencyInjection

核心型別:

  • IServiceCollection -負責服務的註冊
  • ServiceDescriptor -每一個服務註冊的資訊
  • IServiceProvider -必有的容器
  • IServiceScope -表示容器的生命週期

生命週期:ServiceLifeTime

  • 單例 Singleton

    在我們整個根容器的生命週期內都是一個單例。(例如我們用控制檯啟用一個Web Application,只要不關閉此控制檯容器,則單例一直存在。)

  • 作用域 Scoped

    在容器或子容器中,隨著容器的生命週期而改變,若容器釋放,則該依賴物件也會釋放。(如對Web介面每次進行一次請求,則會獲得一個例項,在重新請求後,例項會變化。)

  • 瞬時(暫時)Transient

    每一次請求都能獲得一個新的物件。


我們註冊三個不同生命週期的例項並實現它們各自的抽象介面,來驗證一下:

第一次請求:

第二次請求:

註冊方式:

  • 直接註冊:
services.AddSingleton<IMySingletonService, MySingletonService>();
services.AddScoped<IMyScopedService, MyScopedService>();
services.AddTransient<IMyTransientService, MyTransientService>();
  • 嘗試註冊:
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISample, Sample>());
services.TryAddEnumerable(ServiceDescriptor.Scoped<ISample, Sample>());
services.TryAddEnumerable(ServiceDescriptor.Transient<ISample, Sample>());

  它的優點在於我們想拓展我們的服務時,在同一個介面需要多種實現時,可以避免出現重複的實現。

  • 移除或替換註冊:
services.RemoveAll(ISample);
services.Replace(ServiceDescriptor.Scoped<ISample, Sample>());

獲取依賴注入的方法:

  • 通過建構函式注入依賴 -推薦在在服務大部分介面都需要使用的情況下
  • 通過[FromService]註解 -此服務在某個介面需要使用的情況下

從 main 呼叫服務:

 使用IServiceScopeFactory.CreateScope建立IServiceScope以解析應用範圍內的作用域服務。此方法可以用於在啟動時訪問有作用域的服務以便執行初始化任務(如在初始化資料庫資料中使用)。

以下示例演示如何訪問範圍內IMyDependency服務並在Program.Main中呼叫其WriteMessage方法(官方文件示例):

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

自己專案中的用法(為資料庫設定種子資料):

public static void Main(string[] args)
        {
            
            var host = CreateHostBuilder(args).Build();
      
            using var scope = host.Services.CreateScope();

            var services = scope.ServiceProvider;
            var loggerFactory = services.GetRequiredService<ILoggerFactory>();

            try
            {
                var myContext = services.GetRequiredService<MyContext>();
                MyContextSeed.SeedAsync(myContext, loggerFactory).Wait();

            }
            catch (Exception e)
            {
                var logger = loggerFactory.CreateLogger<Program>();
                logger.LogError(e, "Error occurred when seeding the Database");

            }          

            host.Run();
        }

作用域對注入的影響:

我們在請求服務時,不同的作用域和依賴注入的方式的不同使得容器對服務的釋放方式也不同。

我們建立一個實現類,它實現了它的抽象介面以及IDisposable介面(它的主要職責是在釋放時輸出該服務的HashCode):

  • Transient

首先在StartUp註冊服務,然後將註冊的服務注入到Controller中的兩個物件。

Controller

[HttpGet]
public IActionResult GetDisposed(
[FromServices] IOrderService orderService1,
[FromServices] IOrderService orderService2)
{

  Console.WriteLine("Interface Request Ending");
  return NoContent();
}

執行後發現,這兩個物件的HashCode是不同的,瞬時服務在每一次獲取的時候都會獲取一個新的物件,並且釋放時機是在請求結束後釋放的。

Tips:不建議在根容器中獲取瞬時物件,因為根容器獲取的服務只有在應用程式結束才能釋放。為了防止資源不能及時回收的問題,應避免此用法。

  • Scoped

首先在StartUp註冊服務.

Controller:

[HttpGet]
public IActionResult GetDisposed(
    [FromServices] IOrderService orderService1,
    [FromServices] IOrderService orderService2)
{
    Console.WriteLine("------------1-------------");
    using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
    {
        var service1= scope.ServiceProvider.GetService<IOrderService>();
        var service2 = scope.ServiceProvider.GetService<IOrderService>();
    }
    Console.WriteLine("------------2-------------");

    Console.WriteLine("Interface Request Ending");
    return NoContent();
}    

在此程式碼中的HttpContext.RequestServices是指當前Web請求的容器,也就是應用程式根容器的一個子容器。所以在using語句塊中在子容器作用域中又獲取了一次服務,在using語句塊結束後會自動釋放。

那麼我們從程式碼中可以看出在外層容器中我們請求了兩次服務,內層容器中請求了兩次服務。那是否會出現四次服務呢?

結果是隻出現了兩個不同的物件,說明在每一個作用域中,我們都只能獲取相同的物件。

  • Singleton

結果顯而易見,我們無法釋放我們的服務。因為我們的單例模式是註冊在我們的根容器中的,只有在整個程式退出時去釋放容器所管理的IDisposable的物件。