一、簡介 閱讀 Abp 源碼的過程中,自己也學習到了一些之前沒有接觸過的知識。在這裡,我在這兒針對研究學習 Abp 框架中,遇到的一些值得分享的知識寫幾篇文章。如果有什麼疑問或者問題,歡迎大家評論指正。 在本篇主要是 Scoped 範圍與 using 語句塊的使用。using 語句塊大家一定都不陌生 ...
一、簡介
閱讀 Abp 源碼的過程中,自己也學習到了一些之前沒有接觸過的知識。在這裡,我在這兒針對研究學習 Abp 框架中,遇到的一些值得分享的知識寫幾篇文章。如果有什麼疑問或者問題,歡迎大家評論指正。
在本篇主要是 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 語句塊的時候都應該被釋放。
例如下麵的代碼:
public void TestMethod()
{
using(var scopedResolver = new ScopedResolver())
{
var a = scopedResolver.Resolve<A>();
var b = scopedResolver.Reslove<B>();
}
// 出了語句塊之後 a b 對象自動釋放
}
其實這裡也是利用了 using 語句塊的特性,在 ScopedResolver
類型的定義當中,也實現了 IDisopse
介面。所以在 using 語句塊結束的時候,會自動調用 ScopedResovler
的 Dispose()
方法,在這個方法內部則對已經解析出來的對象調用其 Dispose()
進行釋放。
二、分析
2.0 釋放委托
也是不知道叫什麼標題了,這玩意兒是 Abp 封裝的一個類型,它的作用就是在 using 語句塊結束的時候,執行你傳入的委托。
使用方法如下:
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
介面繼承自 IIocResolver
和 IDisposable
介面,它的本質就是作為 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));
}
所以在這裡,它是通過 SessionOverrideScopeProvider
的 BegionScope()
方法創建了可以被 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
,它的內部可能略微複雜,牽扯到了另一個介面 IAmbientDataContext
和 ScopeItem
類型。
這兩個類型一個是上下文,一個是包裹具體臨時值對象的類型。我們先從 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# 程式設計的理解。
三、結語
通過學習 Abp 框架,也瞭解了自己在基礎方面的諸多不足。其次也是能夠看到一些比較實用新奇的寫法,你也可以在自己項目中進行應用,本文主要是起一個拋磚引玉的作用。最近年底了,事情也比較多,博客也是疏於更新。後面會陸續恢復博文更新,儘量 2 天 1 更,新年新氣象。