.NET

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

2018-09-16  本文已影响45人  冰麟轻武

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

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

一、

一定有人跟我一样想过,在任何时候都可以轻易的得到一个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

上一篇下一篇

猜你喜欢

热点阅读