.NET Core 依賴注入改造(5)- Context

.NET Core 依賴注入改造(1)- 命名服務(wù)
.NET Core 依賴注入改造(2)- 委托轉(zhuǎn)換
.NET Core 依賴注入改造(3)- ILogger
.NET Core 依賴注入改造(4)- ActivatorUtilities
.NET Core 依賴注入改造(5)- Context

.NET Core 依賴注入改造(附1)- Autowired

一闷叉、

一定有人跟我一樣想過趁蕊,在任何時候都可以輕易的得到一個IServerProvider
在Web項目中我們可以通過HttpContext.RequestServices來獲取鹿蜀,但是其他項目目前官方還沒有這樣的上下文對象可用夏哭;
所以老規(guī)矩自己改造一個检柬。

二、

真正改造之前竖配,需要了解一下:Scope何址。
Scope可以理解為作用域

ServiceScope為某個ServiceProvider對象圈定了一個“作用域”进胯,枚舉類型ServiceLifetime中的Scoped選項指的就是這么一個ServiceScope用爪。在依賴注入的應(yīng)用編程接口中,ServiceScope通過一個名為IServiceScope的接口來表示胁镐。如下面的代碼片段所示偎血,繼承自IDisposable接口的IServiceScope具有一個唯一的只讀屬性ServiceProvider返回確定這個服務(wù)范圍邊界的ServiceProvider。表示ServiceScope由它對應(yīng)的工廠ServiceScopeFactory來創(chuàng)建盯漂,后者體現(xiàn)為具有如下定義的接口IServiceScopeFactory颇玷。

摘自:artech

三、

Scope的創(chuàng)建就缆,正如上面所說帖渠,涉及到了三個對象IServiceProviderIServiceScopeFactoryIServiceScope

IServiceProvider 中獲取 IServiceScopeFactory 服務(wù)竭宰,創(chuàng)建 IServiceScope空郊,從Scope中得到限定作用域的 IServiceProvider

用代碼來表示就是:

IServiceProvider serviceProvider = ...份招;
var factory = (IServiceScopeFactory)serviceProvider.GetService(typeof(IServiceScopeFactory));
using(var scope = factory.CreateScope())
{
    var provider = scope.ServiceProvider;
}

ps:IServiceScope同時也是一個IDispose對象,這是非常重要的狞甚,這可以使我們方便的跟蹤IServiceScope生命周期

Microsoft.Extensions.DependencyInjection擴展方法

四脾还、

正確的Scope使用方式應(yīng)該是有層級的;
同一線程同一作用域中同一層級Scope應(yīng)該只有一個
如:

using (var scope1 = _provider.CreateScope())
{
    using (var scope2 = scope1.ServiceProvider.CreateScope())
    {
        Parallel.For(0, 10, i => // 異步時入愧,不同線程可以存在同一層級的Scope
        {
            using (var scope3 = scope2.ServiceProvider.CreateScope())
            {
                using (var scope4 = scope3.ServiceProvider.CreateScope())
                {

                }
            }
        });
    }
}

下面這種用法是錯的:scope1/2/3/4都屬于同一層級;

using (var scope1 = _provider.CreateScope())
using (var scope2 = _provider.CreateScope())
using (var scope3 = _provider.CreateScope())
using (var scope4 = _provider.CreateScope())
{
    action(scope1);
    action(scope2);
    action(scope3);
    action(scope4);
    // 在這種情況下鄙漏,上下文中存在多個同級作用域, Scope 無法確定
}

五棺蛛、

了解了上面這些東西之后怔蚌,自己要做一個服務(wù)上下文還是比較簡單的,首先分別創(chuàng)建IServiceProvider旁赊,IServiceScopeFactoryIServiceScope3個對象的裝飾類桦踊,在不改變原有邏輯的基礎(chǔ)上,增加新的行為终畅。
在裝飾類中拓展Scope的創(chuàng)建和銷毀行為籍胯,創(chuàng)建時將Scope中的IServiceProvider放到上下文中,在Scope銷毀時离福,從上下文中移除杖狼,并將之前的IServiceProvider重新放進去。

源碼在這里
下面摘取部分重要代碼

IServiceProvider裝飾類

class SupportContextServiceProvider : IServiceProvider
{
    private readonly IServiceProvider _provider;
    public SupportContextServiceProvider Parent { get; }
    public SupportContextServiceProvider Root { get; }
    public SupportContextServiceProvider(IServiceProvider provider, SupportContextServiceProvider parent)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
        Parent = parent;
        Root = parent?.Root ?? this;
        ServiceContext.Push(this);  // 設(shè)置到上下文
    }
    private int _disposed;
    public bool IsDisposed => _disposed > 0;
    internal void Dispose()
    {
        if (_disposed == 0 && Interlocked.Increment(ref _disposed) == 1)
        {
            ServiceContext.PopTo(Parent);  // 重置上下文到上一層
        }
    }

    public object GetService(Type serviceType)
    {
        var value = _provider.GetService(serviceType);
        if (value is IServiceScopeFactory factory)
        {
            return new SupportContextServiceScopeFactory(this, factory); // 裝飾Factory
        }
        if (ReferenceEquals(value, _provider))
        {
            return this; // 裝到底
        }
        return value;
    }
}

這個類主要用于將 IServiceProvider 設(shè)置到上下文妖爷,另外對IServiceScopeFactory服務(wù)進行裝飾

IServiceScopeFactory裝飾類

class SupportContextServiceScopeFactory : IServiceScopeFactory
{
    private readonly SupportContextServiceProvider _provider;
    private readonly IServiceScopeFactory _factory;

    public SupportContextServiceScopeFactory(SupportContextServiceProvider provider, IServiceScopeFactory factory)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
        _factory = factory ?? throw new ArgumentNullException(nameof(factory));
    }

    public IServiceScope CreateScope() => new SupportContextServiceScope(_provider, _factory.CreateScope());
}

這個類只做一件事蝶涩,裝飾IServiceScope

IServiceScope 裝飾類

class SupportContextServiceScope : IServiceScope
{
    private readonly IServiceScope _scope;

    public SupportContextServiceScope(SupportContextServiceProvider parent, IServiceScope scope)
    {
        if (parent == null)
        {
            throw new ArgumentNullException(nameof(parent));
        }
        _scope = scope ?? throw new ArgumentNullException(nameof(scope));
        ServiceProvider = new SupportContextServiceProvider(scope.ServiceProvider, parent);
    }

    public IServiceProvider ServiceProvider { get; }

    public void Dispose()
    {
        _scope.Dispose();
        ((SupportContextServiceProvider)ServiceProvider).Dispose();
    }
    ~SupportContextServiceScope()
    {
        ((SupportContextServiceProvider)ServiceProvider).Dispose();
    }
}

做2件事,裝飾IServiceProvider絮识,并在銷毀時調(diào)用SupportContextServiceProvider.Dispose

六绿聘、

現(xiàn)在就剩下一個上下文對象 ServiceContext,這個對象比較復(fù)雜次舌,所以放到最后來再講熄攘;
首先,在.net core中有一個對象是專門用來處理類似“上下文”這種需求的AsyncLocal<T>彼念;

基于任務(wù)的異步編程模型傾向于抽象的線程挪圾,使用AsyncLocal<T>實例可用于跨線程保存數(shù)據(jù)。

但是考慮跨線程銷毀Scope的情況(雖然使用中需要避免這種情況)国拇,但代碼還是要嚴(yán)謹(jǐn)洛史;
所以不能直接使用AsyncLocal<IServiceProvider>惯殊;
使用一個ServiceProviderAccessor來訪問酱吝;
而這個ServiceProviderAccessor只做一件事,當(dāng)provider被標(biāo)識為IsDisposed時返回provider.Parent

[DebuggerDisplay("{DebugText}")]
class ServiceProviderAccessor
{
    public ServiceProviderAccessor(SupportContextServiceProvider provider) => _provider = provider;

    private SupportContextServiceProvider _provider;

    internal SupportContextServiceProvider Provider
    {
        get
        {
            var current = _provider;
            while (current?.IsDisposed == true)
            {
                _provider = current = current.Parent;
            }
            return current;
        }
    }

    private string DebugText() => 
        $"Provider: {_provider}{(_provider?.IsDisposed == true ? " (disposed)" : "")}";
}

他的初始化就放在IServiceProvider裝飾類里土思;

class SupportContextServiceProvider : IServiceProvider
{
    private SupportContextServiceProvider() => Accessor = new ServiceProviderAccessor(this);
    internal ServiceProviderAccessor Accessor { get; }
}

ServiceContext 上下文

public static class ServiceContext
{
    private static AsyncLocal<ServiceProviderAccessor> _value = 
               new AsyncLocal<ServiceProviderAccessor>(LocalValueChanged);

    public static IServiceProvider Provider => _value.Value?.Provider;

    private static SupportContextServiceProvider ProviderImpl
    {
        get => _value.Value?.Provider;
        set
        {
            var accessor = value.Accessor;
            if (!ReferenceEquals(accessor, _value.Value))
            {
                _value.Value = value.Accessor;
            }
        }
    }
    internal static void Push(SupportContextServiceProvider provider)
    {
        if (provider != null)
        {
            ProviderImpl = provider;
        }
    }
    internal static bool PopTo(SupportContextServiceProvider provider)
    {
        provider = provider.Accessor.Provider;
        if (provider != null)
        {
            ProviderImpl = provider;
        }
    }

    private static void LocalValueChanged(AsyncLocalValueChangedArgs<ServiceProviderAccessor> obj)
    {
        if (obj.ThreadContextChanged)
        {
            var prev = obj.PreviousValue?.Provider;
            var curr = obj.CurrentValue?.Provider;
            if (curr == null || prev?.IsDisposed == false)
            {
                ProviderImpl = prev;
            }
        }
    }
}

LocalValueChanged方法是當(dāng)AsyncLocal<T>值發(fā)生變更時被調(diào)用的务热;
其中obj.ThreadContextChanged用于指示是否是由于上下文切換引起的值改變忆嗜;
當(dāng)因為線程切換發(fā)生Scope變更時,如果前一個Scope還沒有銷毀崎岂,那么就帶回來捆毫;
為了處理類似這種情況:

IServiceProvider provider = ...;
IServiceScope scope; //上下文 = provider
await Task.Run(() =>
{
    scope = provider.CreateScope(); // 上下文 = scope
});
action(scope); // 上下文 = provider (這里顯然是錯的)
scope.Dispose();

有看官可能會說了,哪有人寫這樣的代碼...
那我給他換個樣子:

IServiceProvider provider = ...;
using (IServiceScope scope = await CreateScopeAsync(provider))
{
    action(scope);
}

與剛才那個是一回事冲甘;
再來體會下這句話

當(dāng)因為線程切換發(fā)生Scope變更時绩卤,如果前一個Scope還沒有銷毀,那么就帶回來

ServiceContextFactory

public static class ServiceContextFactory
{
    public static IServiceProvider Create(IServiceProvider provider) =>
        new SupportContextServiceProvider(provider, null);
}

七江醇、

在 Core Web 中測試一下:
先在 Startup.ConfigureServices 創(chuàng)建支持上下文的服務(wù)提供程序

public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        return ServiceContextFactory.Create(services.BuildServiceProvider());
    }
}

然后在Controller中驗證下

[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    var b = ReferenceEquals(HttpContext.RequestServices, ServiceContext.Provider);
    return new string[] { "value1", "value2"};
}

結(jié)果


true

八濒憋、

github:https://github.com/blqw/blqw.DI/tree/master/src/blqw.DI.Context
nuget:https://www.nuget.org/packages/blqw.DI.Context

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市陶夜,隨后出現(xiàn)的幾起案子凛驮,更是在濱河造成了極大的恐慌,老刑警劉巖条辟,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黔夭,死亡現(xiàn)場離奇詭異,居然都是意外死亡羽嫡,警方通過查閱死者的電腦和手機本姥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杭棵,“玉大人扣草,你說我怎么就攤上這事⊙胀溃” “怎么了辰妙?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甫窟。 經(jīng)常有香客問我密浑,道長,這世上最難降的妖魔是什么粗井? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任尔破,我火速辦了婚禮,結(jié)果婚禮上浇衬,老公的妹妹穿的比我還像新娘懒构。我一直安慰自己,他們只是感情好耘擂,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布胆剧。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秩霍。 梳的紋絲不亂的頭發(fā)上篙悯,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音铃绒,去河邊找鬼鸽照。 笑死,一個胖子當(dāng)著我的面吹牛颠悬,可吹牛的內(nèi)容都是我干的矮燎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼赔癌,長吁一口氣:“原來是場噩夢啊……” “哼漏峰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起届榄,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤浅乔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后铝条,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體靖苇,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年班缰,在試婚紗的時候發(fā)現(xiàn)自己被綠了贤壁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡埠忘,死狀恐怖脾拆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情莹妒,我是刑警寧澤名船,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站旨怠,受9級特大地震影響渠驼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鉴腻,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一迷扇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爽哎,春花似錦蜓席、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春隘庄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背癣亚。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工丑掺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人述雾。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓街州,卻偏偏與公主長得像,于是被迫代替她去往敵國和親玻孟。 傳聞我的和親對象是個殘疾皇子唆缴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容