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

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示例

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

👇感謝閱讀,點贊+分享+收藏+關注👇

公衆號:猿惑豁

寫作不易,轉載請註明博文地址,否則禁轉!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章