如何使用C#中的Lambda表達式操作Redis Hash結構,簡化緩存中對象屬性的讀寫操作

来源:https://www.cnblogs.com/anech/archive/2023/07/15/17556457.html
-Advertisement-
Play Games

Redis是一個開源的、高性能的、基於記憶體的鍵值資料庫,它支持多種數據結構,如字元串、列表、集合、散列、有序集合等。其中,Redis的散列(Hash)結構是一個常用的結構,今天跟大家分享一個我的日常操作,如何使用Redis的散列(Hash)結構來緩存和查詢對象的屬性值,以及如何用Lambda表達式樹 ...


Redis是一個開源的、高性能的、基於記憶體的鍵值資料庫,它支持多種數據結構,如字元串、列表、集合、散列、有序集合等。其中,Redis的散列(Hash)結構是一個常用的結構,今天跟大家分享一個我的日常操作,如何使用Redis的散列(Hash)結構來緩存和查詢對象的屬性值,以及如何用Lambda表達式樹來簡化這個過程。

一、什麼是Redis Hash結構

Redis Hash結構是一種鍵值對的集合,它可以存儲一個對象的多個欄位和值。例如,我們可以用一個Hash結構來存儲一個人的信息,如下所示:

HSET person:1 id 1
HSET person:1 name Alice
HSET person:1 age 20

上面的命令將一個人的信息存儲到了一個名為person:1的Hash結構中,其中每個欄位都有一個名稱和一個值。我們可以使用HGET命令來獲取某個欄位的值,例如:

HGET person:1 name#Alice

我們也可以使用HGETALL命令來獲取所有欄位的值,例如:

HGETALL person:1id 1name Aliceage 20

二、如何使用C#來操作Redis Hash結構

為了在C#中操作Redis Hash結構,我們需要使用一個第三方庫:StackExchange.Redis。這個庫提供了一個ConnectionMultiplexer類,用於創建和管理與Redis伺服器的連接,以及一個IDatabase介面,用於執行各種命令。例如,我們可以使用以下代碼來創建一個連接對象和一個資料庫對象:

// 連接Redis伺服器
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
// 獲取資料庫對象IDatabase db = redis.GetDatabase();

然後,我們可以使用db對象的HashSet方法和HashGet方法來存儲和獲取Hash結構中的欄位值。

// 創建一個HashEntry數組,存放要緩存的對象屬性
HashEntry[] hashfield = new HashEntry[3];
hashfield[0] = new HashEntry("id", "1");
hashfield[1] = new HashEntry("name", "Alice");
hashfield[2] = new HashEntry("age", "20");

// 使用HashSet方法將對象屬性緩存到Redis的散列(Hash)結構中
db.HashSet("person:1", hashfield);
// 使用HashGetAll方法從Redis的散列(Hash)結構中查詢對象屬性
HashEntry[] result = db.HashGetAll("person:1");
// 遍歷結果數組,列印對象屬性
foreach (var item in result)
{
    Console.WriteLine(item.Name + ": " + item.Value);
}

但是,這種方式有一些缺點:

  • 首先,我們需要手動將對象的屬性名和值轉換為HashEntry數組,並且保持一致性。
  • 其次,我們需要使用字元串來指定要存儲或獲取的欄位名,並且還要避免拼寫錯誤或重覆。
  • 最後,我們需要手動將返回的RedisValue類型轉換為我們需要的類型。

有沒有更優雅的方法來解決這個問題呢?答案是肯定的。

三、如何用Lambda表達式輕鬆操作Redis Hash結構

Lambda表達式是一種匿名函數,可以用來表示委托或表達式樹。在.NET中,我們可以使用Lambda表達式來操作實體類的屬性,比如獲取屬性的值或者更新屬性的值。

我們可以利用 Lambda表達式來指定要存儲或獲取的對象的屬性,而不是使用字元串。使用表達式樹來遍歷Lambda表達式,提取出屬性名和屬性值,並轉換為HashEntry數組或RedisValue數組,使其更易於使用。例如:

Get<Person>(p => new { p.Name, p.Age });

如果我們只想選擇一個屬性,就可以直接寫:

Get<Person>(p => p.Name)

如果要更新對象指定的屬性,可以這樣寫了:

Update<Person>(p => p
    .SetProperty(x => x.Name, "Alice") 
    .SetProperty(x => x.Age, 25));

怎麼樣,這樣是不是優雅多了,這樣做有以下好處:

  • 代碼更加可讀和可維護,因為我們可以直接使用對象的屬性,而不是使用字元串。
  • 代碼更加穩定和精確,因為我們可以避免拼寫錯誤或重覆,並且可以利用編譯器的類型檢查和提示。

那麼,我們如何實現上面的方法呢?

1、Get方法

這個方法的目的是從緩存中獲取對象的一個或多個屬性值,使用一個泛型方法和一個Lambda表達式來實現。

private static TResult Get<T, TResult>(IDatabase db, int id, Expression<Func<T, TResult>> selector)
{
    if (selector == null)
        throw new ArgumentNullException(nameof(selector));

    // 使用擴展方法獲取要查詢的屬性名數組
    var hashFields = selector.GetMemberNames().Select(m => new RedisValue(m)).ToArray();
    // 從緩存中獲取對應的屬性值數組
    var values = db.HashGet($"person:{id}", hashFields);
    // 使用擴展方法將HashEntry數組轉換為對象
    var obj = values.ToObject<T>(hashFields);
    // 返回查詢結果
    return selector.Compile()(obj);
}

private static TResult Get<TResult>(IDatabase db, int id, Expression<Func<Person, TResult>> selector)
    => Get<Person, TResult>(db, id, selector);
  • 首先,定義一個泛型方法Get<T, TResult>,它接受一個資料庫對象db,一個對象id,和一個Lambda表達式selector作為參數。這個Lambda表達式的類型是Expression<Func<T, TResult>>,表示它接受一個T類型的對象,並返回一個TResult類型的結果。這個Lambda表達式的作用是指定要查詢的屬性。
  • 然後,在Get<T, TResult>方法中,首先判斷selector是否為空,如果為空,則拋出異常。然後,使用擴展方法GetMemberNames來獲取selector中的屬性名數組,並轉換為RedisValue數組hashFields。這個擴展方法使用了ExpressionVisitor類來遍歷表達式樹,並重寫了VisitMember方法來獲取屬性名。接下來,使用db.HashGet方法從緩存中獲取對應的屬性值數組values,使用id作為鍵。然後,使用擴展方法ToObject來將values數組轉換為T類型的對象obj。這個擴展方法使用了反射來獲取T類型的屬性,並設置對應的屬性值和類型轉換。最後,返回selector編譯後並傳入obj作為參數的結果。
  • 接下來,定義一個私有方法Get<TResult>,它接受一個資料庫對象db,一個對象id,和一個Lambda表達式selector作為參數。這個Lambda表達式的類型是Expression<Func<Person, TResult>>,表示它接受一個Person類型的對象,並返回一個TResult類型的結果。這個Lambda表達式的作用是指定要查詢的Person對象的屬性。
  • 然後,在Get<TResult>方法中,直接調用Get<T, TResult>方法,並傳入db,id,selector作為參數,並指定T類型為Person。這樣,就可以得到一個TResult類型的結果。

2、MemberExpressionVisitor擴展類

這個類的作用是遍歷一個表達式樹,收集其中的成員表達式的名稱,並存儲到一個列表中。

public class MemberExpressionVisitor : ExpressionVisitor
{
    private readonly IList<string> _names;

    public MemberExpressionVisitor(IList<string> list)
    {
        _names = list;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var name = node.Member.Name; 
        if (node.Expression is MemberExpression member)
        {
            Visit(member); 
            name = member.Member.Name + "." + name; 
        }
        _names.Add(name); 

        return base.VisitMember(node);
    }
}
  • 首先,定義一個類MemberExpressionVisitor,它繼承自ExpressionVisitor類。這個類有一個私有欄位_names,用於存儲屬性名。它還有一個構造函數,接受一個IList<string>類型的參數list,並將其賦值給_names欄位。
  • 然後,在MemberExpressionVisitor類中,重寫了VisitMember方法,這個方法接受一個MemberExpression類型的參數node。這個方法的作用是訪問表達式樹中的成員表達式節點,並獲取其屬性名。
  • 接下來,在VisitMember方法中,首先獲取node節點的屬性名,並賦值給name變數。然後判斷node節點的表達式是否是另一個成員表達式,如果是,則遞歸地訪問該表達式,並將其屬性名和name變數用"."連接起來,形成一個屬性路徑。然後將name變數添加到_names集合中。最後返回基類的VisitMember方法的結果。

3、Update方法

這個方法目的是將一個對象指定的屬性名和值更新到緩存中,使用一個泛型方法和一個委托函數來實現。

public static Dictionary<string, object> Update<TSource>(Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>> setPropertyCalls)
{
    if (setPropertyCalls == null)
        throw new ArgumentNullException(nameof(setPropertyCalls));

    var nameValues = new Dictionary<string, object>(100); // 創建一個字典用於存儲屬性名和值

    var calls = new SetPropertyCalls<TSource>(nameValues); // 創建一個SetPropertyCalls對象

    setPropertyCalls(calls); // 調用傳入的函數,將屬性名和值添加到字典中

    return nameValues; // 返回字典
}

private static void Update(IDatabase db, int id, Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>> setPropertyCalls)
{
    var hashEntries = Update(setPropertyCalls)
        .Select(kv => new HashEntry(kv.Key, kv.Value != null ? kv.Value.ToString() : RedisValue.EmptyString))
        .ToArray();

    // 將HashEntry數組存儲到緩存中,使用對象的Id作為鍵
    db.HashSet(id.ToString(), hashEntries);
}}
  • 首先,定義一個泛型方法Update<TSource>,它接受一個函數作為參數,這個函數的類型是Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>,表示它接受一個SetPropertyCalls<TSource>對象,並返回一個SetPropertyCalls<TSource>對象。這個函數的作用是設置要更新的屬性名和值。
  • 然後,在Update<TSource>方法中,創建一個字典nameValues,用於存儲屬性名和值。創建一個SetPropertyCalls<TSource>對象calls,傳入nameValues作為構造參數。調用傳入的函數setPropertyCalls,並傳入calls作為參數。這樣,setPropertyCalls函數就可以通過調用calls的SetProperty方法來添加屬性名和值到nameValues字典中。最後,返回nameValues字典。
  • 接下來,定義一個私有方法Update,它接受一個資料庫對象db,一個對象id,和一個函數setPropertyCalls作為參數。這個函數的類型是Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>>,表示它接受一個SetPropertyCalls<Person>對象,並返回一個SetPropertyCalls<Person>對象。這個函數的作用是設置要更新的Person對象的屬性名和值。
  • 然後,在Update方法中,調用Update(setPropertyCalls)方法,並傳入setPropertyCalls作為參數。這樣,就可以得到一個字典nameValues,包含了要更新的Person對象的屬性名和值。將nameValues字典轉換為HashEntry數組hashEntries,使用屬性值的字元串表示作為HashEntry的值。如果屬性值為空,則使用RedisValue.EmptyString作為HashEntry的值。最後,使用db.HashSet方法將hashEntries數組存儲到緩存中,使用id作為鍵。

4、SetPropertyCalls泛型類

這個類的作用是收集一個源對象的屬性名稱和值的對應關係,並提供一個鏈式調用的方法,用於設置屬性的值。

public class SetPropertyCalls<TSource>
{
    private readonly Dictionary<string, object> _nameValues;

    public SetPropertyCalls(Dictionary<string, object> nameValues)
    {
        _nameValues = nameValues;
    }

    public SetPropertyCalls<TSource> SetProperty<TProperty>(Expression<Func<TSource, TProperty>> propertyExpression, TProperty valueExpression)
    {
        if (propertyExpression == null)
            throw new ArgumentNullException(nameof(propertyExpression));

        if (propertyExpression.Body is MemberExpression member && member.Member is PropertyInfo property)
        {
            if (!_nameValues.TryAdd(property.Name, valueExpression))
            {
                throw new ArgumentException($"The property '{property.Name}' has already been set.");
            }
        }
        return this;
    }
}
  • 首先,這個類有一個構造函數,接受一個Dictionary<string, object>類型的參數,作為存儲屬性名稱和值的對應關係的字典,並賦值給一個私有欄位_nameValues。
  • 然後,這個類有一個泛型方法,叫做SetProperty。這個方法接受兩個參數,一個是表示源對象屬性的表達式,另一個是表示屬性值的表達式。
  • 在這個方法中,首先判斷第一個參數是否為空,如果為空,則拋出ArgumentNullException異常。
  • 然後判斷第一個參數的表達式體是否是一個成員表達式,並且該成員表達式的成員是否是一個屬性,如果是,則獲取該屬性的名稱,並賦值給一個局部變數property。
  • 然後嘗試將該屬性名稱和第二個參數的值添加到_nameValues字典中,如果添加失敗,則說明該屬性已經被設置過了,拋出ArgumentException異常。
  • 最後,返回當前對象的引用,實現鏈式調用的效果。

這樣,我們就可以得到一個包含所有要更新的屬性名和值的字典,然後我們就可以根據這些屬性名和值來更新實體類的屬性了。

Demo示例

讓我們來看一下代碼示例,為了方便演示和閱讀,這是臨時碼的,實際中大家可以根據自己習慣來進行封裝,簡化調用,同時也可以使用靜態字典來緩存編譯好的委托及對象屬性,提高性能。


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

-Advertisement-
Play Games
更多相關文章
  • """ # 一、axios是什麼 Axios 是一個基於 promise 網路請求庫,作用於node.js 和瀏覽器中。 它是 isomorphic 的(即同一套代碼可以運行在瀏覽器和node.js中)。在服務端它使用原生 node.js http 模塊, 而在客戶端 (瀏覽端) 則使用 XMLHt ...
  • 中大型項目中,我們都會把項目結構劃分多個模塊。它清晰的定義,便於項目結果維護,同時在日常代碼變更時,各個模塊的隔離也一定程度上保證了變更質量…… ...
  • python的開發工具有很多款,很多都是非常好用的,其中vscode作為其中一款Python的開發工具,是非常輕量級的,今天我們來介紹一下vs code的下載與安裝。 # vscode的下載與安裝 首先需要到vscode的官網,這個谷歌或者百度一下就可以搜到,然後根據你的系統下載你對應的版本,我這裡 ...
  • 第三方鏡像是在Docker Hub或其他容器註冊表上提供的預構建Docker容器鏡像。這些鏡像由個人或組織創建和維護,可以作為您容器化應用程式的起點。 ### 查找第三方鏡像 [**Docker Hub**](https://hub.docker.com/) 是最大和最受歡迎的容器鏡像註冊表,包含官 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《Java擴展Nginx》系列的第 ...
  • # Scala基礎篇 ## 數據類型 下表中列出的數據類型都是對象,可以直接對它們調用方法。 | 數據類型 | 描述 | | | | | Byte | 8位有符號補碼整數。數值區間為 -128 到 127 | | Short | 16位有符號補碼整數。數值區間為 -32768 到 32767 | | ...
  • 在Revit自帶的導出功能中,我們可以知道,Revit可以導出如下格式文件: 他們分別對應的API在Document類下麵,主要包含以下方法 1 Export(String, String, MassGBXMLExportOptions) 從體量模型文檔中導出gbXML文件。 2 Export(St ...
  • 上次老周扯了有關主、從實體的話題,本篇咱們再挖一下,主、從實體之間建立的關係,跟咱們常用的一對一、一對多這些關係之間有什麼不同。 先看看咱們從學習資料庫開始就特熟悉的常用關係——多對多、一對一、一對多說起。數據實體之間會建立什麼樣的關係,並不是規則性的,而是要看數據的功能。比如你家養的狗狗和水果(你 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...