C# 結合 using 語句塊的三種實用方法

来源:https://www.cnblogs.com/myzony/archive/2019/01/16/10275256.html
-Advertisement-
Play Games

一、簡介 閱讀 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 語句塊結束的時候,會自動調用 ScopedResovlerDispose() 方法,在這個方法內部則對已經解析出來的對象調用其 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 介面繼承自 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# 程式設計的理解。

三、結語

通過學習 Abp 框架,也瞭解了自己在基礎方面的諸多不足。其次也是能夠看到一些比較實用新奇的寫法,你也可以在自己項目中進行應用,本文主要是起一個拋磚引玉的作用。最近年底了,事情也比較多,博客也是疏於更新。後面會陸續恢復博文更新,儘量 2 天 1 更,新年新氣象。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 實現目標:將log4net的相關操作封裝成一個 .Net Standard類庫 demo地址:https://github.com/PuzzledAlien/log4net_demo/tree/master/DotNetCoreConsole_V2 Step 1 建立解決方案和處理項目相關配置 在當 ...
  • 控制項說明 底部工具欄控制項。 效果演示 其他效果 該界面為仿淘寶UI製作的一個簡單的UI模板,源碼獲取方式請拉至文章末尾。 特色屬性 屬性 屬性說明 Direction(相對佈局) 容器主軸方向。 Flex(相對佈局) 比例因數。 Items 工具欄項集合。 SelectBackColor 選中後背景 ...
  • 一.在asp.net core中使用多個環境 ASP.NET Core 配置是基於運行時環境, 使用環境變數。ASP.NET Core 在應用啟動時讀取環境變數ASPNETCORE_ENVIRONMENT,並將該值存儲在 IHostingEnvironment.EnvironmentName 中。A ...
  • 使用NPOI導入導出excel,已經封裝好Action可以直接調用 導出 效果圖 使用方法 定義導出實體 導出Action 主要代碼 導入導出代碼 配置類型 ExcelConfig ColumnModel ExcelGridModel 示例下載地址: "代碼打包下載" ...
  • .net中的SelectList可以用於前端下拉框的內容填充 譬如:Html.DropdownList(下拉框標簽名稱, SelectList實例) 實際上,上述Html.DropdownList的第二個參數傳入的是一個SelectListItem的集合。此處使用SelectList比較直觀罷了 所 ...
  • 在 .net core 中配置項推薦用 Options 來實現,有一些參數可能必須是用由用戶來配置,不能直接寫成預設值的參數,這樣就需要就 Options 中的參數做一些校驗,否則程式內部可能就會出現一些意想不到的異常,今天介紹一個比較簡單的,通過 `PostConfigure` 的方式來實現Opt... ...
  • 有這麼個非同步方法 private static async Task Compute(int s) { return await Task.Run(() = { if (s s = new List { 1, 2, 3, 4, 5 }; List t = new List(); s.ForEach( ...
  • C# C/S程式一般通過ConfigurationManager類來讀取app.config,其中有個坑爹的地方是ConfigurationManager類自帶緩存,就如Windows服務來說,除非重啟服務,否則手動修改或者通過程式修改app.config文件是不會生效的, 需要調用Configur ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...