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

.NET Core 依赖注入改造(1)- 命名服务
.NET Core 依赖注入改造(2)- 委托转换
.NET Core 依赖注入改造(3)- ILogger
.NET Core 依赖注入改造(4)- ActivatorUtilities
.NET Core 依赖注入改造(5)- Context

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

一、

一定有人跟我一样想过,在任何时候都可以轻易的得到一个IServerProvider
在Web项目中我们可以通过HttpContext.RequestServices来获取,但是其他项目目前官方还没有这样的上下文对象可用;
所以老规矩自己改造一个。

二、

真正改造之前,需要了解一下:Scope
Scope可以理解为作用域

ServiceScope为某个ServiceProvider对象圈定了一个“作用域”,枚举类型ServiceLifetime中的Scoped选项指的就是这么一个ServiceScope。在依赖注入的应用编程接口中,ServiceScope通过一个名为IServiceScope的接口来表示。如下面的代码片段所示,继承自IDisposable接口的IServiceScope具有一个唯一的只读属性ServiceProvider返回确定这个服务范围边界的ServiceProvider。表示ServiceScope由它对应的工厂ServiceScopeFactory来创建,后者体现为具有如下定义的接口IServiceScopeFactory。

摘自:artech

三、

Scope的创建,正如上面所说,涉及到了三个对象IServiceProviderIServiceScopeFactoryIServiceScope

IServiceProvider 中获取 IServiceScopeFactory 服务,创建 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使用方式应该是有层级的;
同一线程同一作用域中同一层级Scope应该只有一个
如:

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 无法确定
}

五、

了解了上面这些东西之后,自己要做一个服务上下文还是比较简单的,首先分别创建IServiceProviderIServiceScopeFactoryIServiceScope3个对象的装饰类,在不改变原有逻辑的基础上,增加新的行为
在装饰类中拓展Scope的创建和销毁行为,创建时将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);  // 设置到上下文
    }
    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 设置到上下文,另外对IServiceScopeFactory服务进行装饰

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,并在销毁时调用SupportContextServiceProvider.Dispose

六、

现在就剩下一个上下文对象 ServiceContext,这个对象比较复杂,所以放到最后来再讲;
首先,在.net core中有一个对象是专门用来处理类似“上下文”这种需求的AsyncLocal<T>

基于任务的异步编程模型倾向于抽象的线程,使用AsyncLocal<T>实例可用于跨线程保存数据。

但是考虑跨线程销毁Scope的情况(虽然使用中需要避免这种情况),但代码还是要严谨;
所以不能直接使用AsyncLocal<IServiceProvider>
使用一个ServiceProviderAccessor来访问;
而这个ServiceProviderAccessor只做一件事,当provider被标识为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方法是当AsyncLocal<T>值发生变更时被调用的;
其中obj.ThreadContextChanged用于指示是否是由于上下文切换引起的值改变;
当因为线程切换发生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);
}

与刚才那个是一回事;
再来体会下这句话

当因为线程切换发生Scope变更时,如果前一个Scope还没有销毁,那么就带回来

ServiceContextFactory

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

七、

在 Core Web 中测试一下:
先在 Startup.ConfigureServices 创建支持上下文的服务提供程序

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"};
}

结果


true

八、

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容