天天看點

dotnet 通過依賴注入的 Scoped 給工作流注入相同的上下文資訊

本文将來聊聊 Microsoft.Extensions.DependencyInjection 這個依賴注入架構的 Scoped 功能的一個應用,這個架構是預設 ASP.NET Core 的核心庫将會預設被引用。而其他 .NET 的應用如 WPF 或 Xamarin 等也可以使用這個庫。是以本文标題就是 dotnet 而不是具體哪個架構 在開發的時候,咱會有一些複雜的邏輯需要多個類合作進行執行,而在使用多個類進行執行的時候,就涉及到上下文資訊的傳遞。例如最簡單的追蹤 Id 的值,假定在多個類組成的多個步驟裡面,因為存在多線程調用的問題,咱在定位問題的時候需要在日志裡面輸出目前步驟所使用的追蹤 Id 是哪個,這樣就運作進行并行多次任務同時執行,同時日志不會亂

盡管本文使用 Scoped 僅作為日志記錄的功能沒能發揮強大的日志庫的作為,但是減弱日志庫是為了提升 DependencyInjection 的強大,是以請小夥伴僅認為日志庫和輸出文本到控制台之間沒有任何差别

dotnet 通過依賴注入的 Scoped 給工作流注入相同的上下文資訊

如上圖,假定有三個步驟,分别是 F1 和 F2 和 F3 三個步驟,此時有3個任務同時進來。而我期望能夠在日志裡面的相關輸出能包含目前的步驟在執行的任務是哪一個

最簡單的方法是在每個步驟的參數裡面傳遞上任務的追蹤 Id 值,此時就可以在每個步驟裡面的輸出添加追蹤資訊

這個方法存在什麼問題?如果我想要多添加額外的參數,此時我需要改一條鍊。另外也沒有發揮 Scoped 的功能

那麼什麼是 Scoped 的功能?在 Microsoft.Extensions.DependencyInjection 提供的對象注入裡面提供了三個不同的方式,第一個是瞬時 Transient 模式,這個模式可以讓每次擷取執行個體的時候,拿到的都是全新的執行個體。第二個是 Singleton 單例,無論在哪裡從這個容器擷取到的都是相同的對象。而最後一個也是最複雜的就是 Scoped 範圍模式,也是本文要來安利大家的功能

先不說 Scoped 的定義,先來聊聊他的作用。在相同的 using 範圍内,嗯,這個 Scoped 是容器的狀态,容器可以通過 CreateScope 方法進入 Scoped 範圍,如下面代碼

// IServiceProvider serviceProvider
using (var serviceScope = serviceProvider.CreateScope())
{
    
}           

複制

此時在一個相同的 serviceScope 執行個體建立的對象,如果這個對象在注入的時候标記了自己是 Scoped 範圍,那麼将會拿到相同的執行個體。而在标記了 Scoped 範圍的對象在不同的 ServiceScope 執行個體建立的是不同的對象,如下面代碼

// IServiceProvider serviceProvider

// Foo 是注冊為 Scoped 的對象
// services.AddScoped<Foo>();

using (var serviceScope1 = serviceProvider.CreateScope())
{
	// 下面代碼的 foo 和 foo1 是相同的對象
    var foo = serviceScope.ServiceProvider.GetService<Foo>();
    var foo1 = serviceScope.ServiceProvider.GetService<Foo>();
}

using (var serviceScope2 = serviceProvider.CreateScope())
{
	// 下面的 foo2 和 foo 不是相同的對象
    var foo2 = serviceScope.ServiceProvider.GetService<Foo>();
}           

複制

是以假設将各個步驟加上步驟需要的上下文資訊類都作為 Scoped 範圍注入,那麼此時在一次任務過程中,任務使用的步驟都在一個 Scoped 裡面,如果此時的任務使用相同的類型的上下文資訊類,那麼此上下文資訊将會是相同的對象。是以各個任務可以使用上下文資訊共享資訊

假設上下文資訊類是 Info 類,裡面隻有使用一個資訊就是 Id 這個資訊

public class Info
    {
        public string Id { set; get; }
    }           

複制

為了友善起見,我還是建立一個 ASP.NET Core 應用,因為這個應用架構預設部署好了依賴注入

在 Startup.cs 裡面進行注冊

public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<Info>();
        }           

複制

然後定義三個步驟的類

public class F1
    {
        public F1(ILogger<F1> logger, Info info, F2 f2)
        {
            _logger = logger;
            Info = info;
            F2 = f2;
        }

        public Info Info { get; }

        public void Do()
        {
            _logger.LogInformation(Info.Id);
            F2.Do();
        }

        private readonly ILogger<F1> _logger;

        private F2 F2 { get; }
    }

    public class F2
    {
        public F2(ILogger<F2> logger, Info info)
        {
            _logger = logger;
            Info = info;
        }

        public Info Info { get; }

        public void Do()
        {
            _logger.LogInformation(Info.Id);
        }

        private readonly ILogger<F2> _logger;
    }

    public class F3
    {
        public F3(ILogger<F3> logger, Info info)
        {
            _logger = logger;
            Info = info;
        }

        public Info Info { get; }

        public void Do()
        {
            _logger.LogInformation(Info.Id);
        }

        private readonly ILogger<F3> _logger;
    }           

複制

可以看到上面三個類都是隻有一個 Do 方法,在 Do 方法裡面調用日志記錄上下文資訊

在 Startup.cs 裡面進行注冊這三個步驟

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddScoped<Info>();

            services.AddScoped<F1>();
            services.AddScoped<F2>();
            services.AddScoped<F3>();
        }           

複制

在使用的時候,預設控制器就是注冊為 Scoped 的,是以在控制器裡面無論是構造注入或者是使用容器擷取都是在相同的 Scoped 裡面

上面代碼是 F1 步驟引用 F2 步驟,咱在構造将 F1 注入。而 F3 是獨立步驟,咱通過容器擷取

[ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        public WeatherForecastController(ILogger<WeatherForecastController> logger, F1 f1, Info info,
            IServiceProvider serviceProvider)
        {
            Info = info;
            _logger = logger;
            _f1 = f1;
            _serviceProvider = serviceProvider;

            using (var serviceScope = serviceProvider.CreateScope())
            {
                var foo = serviceScope.ServiceProvider.GetService<Info>();
                var foo1 = serviceScope.ServiceProvider.GetService<Info>();
                if (ReferenceEquals(foo, foo1))
                {

                }
            }
        }

        public Info Info { get; }

        [HttpGet]
        public IActionResult Get()
        {
            Info.Id = DateTime.Now.ToString();
            _logger.LogInformation(Info.Id);
            _f1.Do();

            var f3 = _serviceProvider.GetService<F3>();
            f3.Do();

            return Ok();
        }

        private readonly F1 _f1;

        private readonly ILogger<WeatherForecastController> _logger;
        private readonly IServiceProvider _serviceProvider;
    }           

複制

執行代碼可以看到 F1 和 F2 和 F3 的 Info 對象都是相同的對象,于是在 Info 對象設定的值可以在三個步驟使用

通過這個方法,在後續修改的時候,假如有一個資訊是 F1 和 F3 都需要的,但是 F1 和 F3 是獨立的,此時就可以再建立一個類用于存放此參數,然後将這個類注冊為 Scoped 的。接着在 F1 和 F3 注入這個類,此時使用的對象就是相同的對象,是以參數也就能傳遞

有趣的是這個方法改動僅僅隻是 F1 和 F3 兩個類加上依賴注入構造,其他子產品可以不動

本文代碼放在 github 歡迎小夥伴通路

本文會經常更新,請閱讀原文: https://blog.lindexi.com/post/dotnet-%E9%80%9A%E8%BF%87%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84-Scoped-%E7%BB%99%E5%B7%A5%E4%BD%9C%E6%B5%81%E6%B3%A8%E5%85%A5%E7%9B%B8%E5%90%8C%E7%9A%84%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BF%A1%E6%81%AF.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。