C# 结合 using 语句块的三种实用方法

1、简介

阅读 Abp 源码的过程当中,本身也学习到了一些以前没有接触过的知识。在这里,我在这儿针对研究学习 Abp 框架中,遇到的一些值得分享的知识写几篇文章。若是有什么疑问或者问题,欢迎你们评论指正。html

在本篇主要是 Scoped 范围与 using 语句块的使用。using 语句块你们必定都不陌生,都是与非托管对象一块儿存在的,它有一个特性就是在 using 语句块结束的时候会调用对象的 IDispose.Dispose() 方法。通常咱们会在非托管类型的 Dispose() 方法内部进行资源的释放,相似于 C 语言的 free() 操做。多线程

例以下面的代码:框架

public void TestMethod()
{
    using(var waitDisposeObj = new TestClass())
    {
        // 执行其余操做 xxx
    }
    
    // 出了语句块以后就,自动调用 waitDisposeObj 的 Dispose() 方法。
}

能够看到上面的例子,using 语句块包裹的就是一个范围 (Scoped)。其实这里能够延伸到依赖注入的概念,在依赖注入的生命周期当中有一个 Scoped 的生命周期。(PS: 须要了解的能够去阅读个人 这篇文章)异步

一个 Scoped 其实就能够看做是一个 using 语句块包裹的范围,全部解析出来的对象在离开 using 语句块的时候都应该被释放。async

例以下面的代码:ide

public void TestMethod()
{
    using(var scopedResolver = new ScopedResolver())
    {
        var a = scopedResolver.Resolve<A>();
        var b = scopedResolver.Reslove<B>();
    }
    
    // 出了语句块以后 a b 对象自动释放
}

其实这里也是利用了 using 语句块的特性,在 ScopedResolver 类型的定义当中,也实现了 IDisopse 接口。因此在 using 语句块结束的时候,会自动调用 ScopedResovlerDispose() 方法,在这个方法内部则对已经解析出来的对象调用其 Dispose() 进行释放。函数

2、分析

2.0 释放委托

也是不知道叫什么标题了,这玩意儿是 Abp 封装的一个类型,它的做用就是在 using 语句块结束的时候,执行你传入的委托。学习

使用方法以下:ui

var completedTask = new DisposeAction(()=>Console.WriteLine("using 语句块结束了。"));
using(completedTask)
{
    // 其余操做
}
// 执行完成以后会调用 completedTask 传入的委托。

根据上述用法,你也应该猜出来这个 DisposeAction 类型的定义了。该类型继承了 IDispose 接口,而且在内部有一个 Action 字段,用于存储构造函数传入的委托。在执行 Dispose() 方法的时候,执行传入的委托。线程

public class DisposeAction : IDisposable
{
    public static readonly DisposeAction Empty = new DisposeAction(null);

    private Action _action;

    public DisposeAction([CanBeNull] Action action)
    {
        _action = action;
    }

    public void Dispose()
    {
        // 防止在多线程环境下,屡次调用 action
        var action = Interlocked.Exchange(ref _action, null);
        action?.Invoke();
    }
}

2.1 统一对象释放

统一对象释放是 Abp 当中的另外一种用法,其实按照 Abp 框架的定义,叫作 ScopedResolver(范围解析器)。顾名思义,经过 ScopedResolver 解析出来的对象,都会在 using 语句块结束以后统一进行销毁。

IScopedIocResolver 接口继承自 IIocResolverIDisposable 接口,它的本质就是做为 Ioc 解析器的一种特殊实现,因此它拥有全部 Ioc 解析器的方法,这里就再也不赘述。

它的实现也比较简单,在其内部有一个集合维护每一次经过 IIocResolver 解析出来的对象。在 Dispose() 方法执行的时候,遍历这个集合,调用 Ioc 解析器的 Release() 方法释放对象并从集合中删除对象。下面就是实现的简化版:

public class ScopedIocResolver : IScopedIocResolver
{
    private readonly IIocResolver _iocResolver;
    private readonly List<object> _resolvedObjects;

    public ScopedIocResolver(IIocResolver iocResolver)
    {
        _iocResolver = iocResolver;
        _resolvedObjects = new List<object>();
    }
    
    // 解析对象
    public object Resolve(Type type)
    {
        var resolvedObject = _iocResolver.Resolve(type);

        // 添加到集合,方便后续释放
        _resolvedObjects.Add(resolvedObject);
        return resolvedObject;
    }
    
    public void Release(object obj)
    {
        // 从集合当中移除
        _resolvedObjects.Remove(obj);
        // 经过 Ioc 管理器释放对象
        _iocResolver.Release(obj);
    }
    
    public void Dispose()
    {
        // 遍历集合,释放对象
        _resolvedObjects.ForEach(_iocResolver.Release);
    }
}

经过 IScopedResolver 解析出来的对象,在 using 语句块结束的时候都会被释放,免去了咱们每次手动释放的操做。

2.2 临时值变动

暂时想不到一个好一点的标题,暂时用这个标题代替吧。这里以 Abp 的一段实例代码为例,在有的时候咱们可能当前的用户没有登陆,因此在 IAbpSession 里面的 UserId 等属性确定是为 NULL 的。而 IAbpSession 在设计的时候,这些属性是不容许更改的。

那么咱们有时候可能会临时更改 IAbpSession 里面关于 UserId 的值怎么办呢?

这个时候能够经过 IAbpSession 提供的一个 IDisposable Use(int tenantId, long? userId, string userCode) 进行临时更改。他拥有一个 Use() 方法,而且返回一个实现了 IDispose 接口的对象,用法通常是这样:

public void TestMethod()
{
    using(AbpSession.Use(1,2,"3"))
    {
       // 内部临时更改了 AbpSession 的值 
    }
    
    // using 语句块结束的时候,调用 Use 返回对象的 Dispose 方法。
}

转到其抽象类 AbpSessionBase 实现,能够看到他的实现是这个样子的:

protected IAmbientScopeProvider<SessionOverride> SessionOverrideScopeProvider { get; }

public IDisposable Use(int tenantId, long? userId, string userCode)
{
    return SessionOverrideScopeProvider.BeginScope(SessionOverrideContextKey, new SessionOverride(null, tenantId, userId, userCode));
}

因此在这里,它是经过 SessionOverrideScopeProviderBegionScope() 方法建立了能够被 Dispose() 的对象。

接着继续跳转,来到 IAmbientScopeProvider 接口定义,这个接口接受一个泛型参数,能够看到以前在 AbpSessionBase 传入了一个 SessionOverride。这个 SessionOverride 就是封装了 UserId 等信息的存储类,也就是说 SessionOverride 就是容许进行临时值更改的类型定义。

在开始执行 BegionScope() 方法的时候,就针对传入的 value 进行存储,获取 Session 值的时候优先读取存储的值,不存在才执行真正的读取,调用 Dispose() 方法的时候就进行释放。

因此接口提供了两个方法,第一个咱们先看 BegionScope() 方法,接收一个 contextKey 用来区分不一样的临时值,第二个参数则是要存储的临时值。

第二个方法为 GetValue,从一个上下文(后面讲)当中根据 contextKey 得到存储的临时值。

public interface IAmbientScopeProvider<T>
{
    T GetValue(string contextKey);

    IDisposable BeginScope(string contextKey, T value);
}

针对于该接口,其默认实现是 DataContextAmbientScopeProvider ,它的内部可能略微复杂,牵扯到了另外一个接口 IAmbientDataContextScopeItem 类型。

这两个类型一个是上下文,一个是包裹具体临时值对象的类型。咱们先从 BeginScope() 方法开始看:

// ScopeItem 的 Id 与其值关联的字典,其键为 Guid,值为具体的 ScopeItem 对象,这里并未与 ContextKey 进行关联。
private static readonly ConcurrentDictionary<string, ScopeItem> ScopeDictionary = new ConcurrentDictionary<string, ScopeItem>();

// 数据的上下文对象,管理 ContextKey 与其 Id。
private readonly IAmbientDataContext _dataContext;

public IDisposable BeginScope(string contextKey, T value)
{
    // 将须要临时存储的对象,用 ScopeItem 包装起来,它的外部对象是当前对象 (若是存在的话)。
    var item = new ScopeItem(value, GetCurrentItem(contextKey));

    // 将包装好的对象以 Id-对象,的形式存储在字典当中。
    if (!ScopeDictionary.TryAdd(item.Id, item))
    {
        throw new AbpException("Can not add item! ScopeDictionary.TryAdd returns false!");
    }

    // 在上下文当中设置当前的 ContextKey 关联的 Id。
    _dataContext.SetData(contextKey, item.Id);

    // 集合释放委托,using 语句块结束时,作释放操做。
    return new DisposeAction(() =>
    {
        // 从字典中移除指定 Id 的对象。
        ScopeDictionary.TryRemove(item.Id, out item);

        // 若是包装对象没有外部对象,直接设置上下文关联的 Id 为 NULL。
        if (item.Outer == null)
        {
            _dataContext.SetData(contextKey, null);
            return;
        }

        // 若是还有外部对象,则设置上下文关联的 Id 为外部对象的 I的。
        _dataContext.SetData(contextKey, item.Outer.Id);
    });
}

从上面的逻辑能够看出来,每次咱们加入的临时值都是经过 ScopeItem 包裹起来的。而这个 ScopeItem 与咱们的工做单元类似,它会有一个外部链接的对象。这个外部链接对象的做用就是解决 using 语句嵌套问题的,例如咱们有如下代码:

public void TestMethod()
{
    using(AbpSession.Use(1,2,"3"))
    {
       // 一些业务逻辑
       // ScopeItem.Outer = null;
       using(AbpSession.Use(4,5,"6"))
       {
           // 一些业务逻辑
           // ScopeItem.Outer = 外部对象;
       }
    }
}

那么咱们在这里会有同一个 ContextKey,都是提供给 AbpSession 使用的。第一次我在 Use() 内部经过 BeginScope() 方法建立了一个 ScopeItem 对象,包装了临时值,这个 ScopeItem 的外部对象为 NULL。第二次我又在内部建立了一个 ScopeItem 对象,包装了第二个临时值,这个时候 ScopeItem 的外部对象就是第一次包装的对象了。

执行释放操做的时候,首先判断外部对象是否为空。若是为空则直接在上下文当中将绑定的 ScopeItem 的 Id 值设为 NULL,若是不为空,则设置为它的外部对象的 Id。

仍是以上面的代码为例,在 Dispose() 被执行以后,由内而外,到最外层的时候在上下文与 ContextKey 关联的 Id 已经被置为 NULL 了。

private ScopeItem GetCurrentItem(string contextKey)
{
    // 从数据上下文获取指定 ContextKey 当前关联的 Id 值。
    var objKey = _dataContext.GetData(contextKey) as string;
    // 不存在则返回 NULL,存在则尝试以 Id 从字典中拿取对象外部,并返回。
    return objKey != null ? ScopeDictionary.GetOrDefault(objKey) : null;
}

分析了一下 IAmbientDataContext 的实现,感受与 ICurrentUnitOfWorkProvider 相似,内部都是经过 AsyncLocal 来进行处理的。

public class AsyncLocalAmbientDataContext : IAmbientDataContext, ISingletonDependency
{
    // 这里的字典是以 ContextKey 与 ScopeItem 的 Id 构成的。
    private static readonly ConcurrentDictionary<string, AsyncLocal<object>> AsyncLocalDictionary = new ConcurrentDictionary<string, AsyncLocal<object>>();

    public void SetData(string key, object value)
    {
        // 设置指定 ContextKey 对应的 Id 值。
        var asyncLocal = AsyncLocalDictionary.GetOrAdd(key, (k) => new AsyncLocal<object>());
        asyncLocal.Value = value;
    }

    public object GetData(string key)
    {
        // 获取指定 ContextKey 对应的 Id 值。
        var asyncLocal = AsyncLocalDictionary.GetOrAdd(key, (k) => new AsyncLocal<object>());
        return asyncLocal.Value;
    }
}

从开始到这里使用并行字典的状况来看,这里这么作的缘由很简单,是为了处理异步上下文切换的状况,确保 ContextKey 对应的 Id 是一致的,防止在 Get/Set Data 的时候出现 意外的状况

最后呢在具体的 Session 实现类 ClaimsAbpSession 当中要获取 UserId 会通过下面的步骤:

public override long? UserId
{
    get
    {
        // 尝试从临时对象中获取数据。
        if (OverridedValue != null)
        {
            return OverridedValue.UserId;
        }

        // 从 JWT Token 当中获取 UserId 信息。

        return userId;
    }
}

最后我再贴上 ScopeItem 的定义。

private class ScopeItem
{
    public string Id { get; }

    public ScopeItem Outer { get; }

    public T Value { get; }

    public ScopeItem(T value, ScopeItem outer = null)
    {
        Id = Guid.NewGuid().ToString();

        Value = value;
        Outer = outer;
    }
}

这个临时值变动多是 Abp 用法当中最为复杂的一个,牵扯到了异步上下文和 using 语句嵌套的问题。但仔细阅读源码以后,其实有一种豁然开朗的感受,也增强了对于 C# 程序设计的理解。

3、结语

经过学习 Abp 框架,也了解了本身在基础方面的诸多不足。其次也是可以看到一些比较实用新奇的写法,你也能够在本身项目中进行应用,本文主要是起一个抛砖引玉的做用。最近年末了,事情也比较多,博客也是疏于更新。后面会陆续恢复博文更新,尽可能 2 天 1 更,新年新气象。

相关文章
相关标签/搜索