1. 程式人生 > 其它 >C#動態構建表示式樹(三)——表示式的組合

C#動態構建表示式樹(三)——表示式的組合

C#動態構建表示式樹(三)——表示式的組合

前言

在篩選資料的過程中,可能會有這樣的情況:有一些查詢條件是公共的,但是根據具體的傳入引數可能需要再額外增加一個條件。對於這種問題一般有兩種方法:

a. 在 Where 後再組合一個 Where,如:

List<SOME_CLASS> dataList = dataList.Where(FILTER_1).Where(FILTER_2).ToList();

b. 將型別相同兩個表示式組合起來(就是本文的主題了)

由於專案中既有框架的封裝,查詢時只能傳入 Expression<Func<T, bool>> 型別的值,因此只能採用 b 方法。

最終

先給出研究後最後得出的方法,再分享自己踩坑的過程(僅以 And 操作為例,其它的大同小異)。

1. 程式碼

public static Expression<Func<T, bool>> CombineLambda<T>(List<Expression<Func<T, bool>>> lambdas, List<Type> types)
{
    if(typeof(T) != types[0])
    {
        throw new Exception("型別列表的第一個值必須為泛型 T 的型別");
    }

    List<ParameterExpression> parameterExpressions = new List<ParameterExpression>();

    // 從 'A' 開始,為每個型別指定一個形參
    for (int i = 0; i < types.Count; i++)
    {
        parameterExpressions.Add(Expression.Parameter(types[i], ((char)(65 + i)).ToString()));
    }
    CombineExpressionVisitor visitor = new CombineExpressionVisitor(parameterExpressions);
    
    // 新建一個原始條件 a => true,並逐個組合陣列中的條件
    Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(
        Expression.Constant(true), parameterExpressions[0]);
    for (int i = 0; i < lambdas.Count; i++)
    {
        Expression tmp = visitor.Visit(result.Body);
        Expression tmp1 = visitor.Visit(lambdas[i].Body);
        result = Expression.Lambda<Func<T, bool>>(Expression.AndAlso(tmp, tmp1), parameterExpressions[0]);
    }
    return result;


    /*
    ** 上面的 type[0](parameterExpressions[0]) 可能比較費解。由於我們的情況是巢狀的對
    ** 象,因此需要傳入 “外層物件型別的 Paramter” 和 “內層物件型別的 Parameter”,
    ** parameterExpressions[0] 表示的是 外層物件型別的 Parameter,由它開始組成** 整個的篩選
     */
}

private class CombineExpressionVisitor : ExpressionVisitor
{
    private List<ParameterExpression> peList;
    public CombineExpressionVisitor(List<ParameterExpression> list)
    {
        peList = list;
    }

    // 覆寫 VisitParameter 方法,返回我們指定的統一 Parameter
    protected override Expression VisitParameter(ParameterExpression p)
    {
        return peList.Find(a => a.Type == p.Type);
    }
    public override Expression Visit(Expression node)
    {
        return base.Visit(node);
    }
}

Lambda表示式由引數(Parameter)內容(Body)組成。主要的思路是 將給出的所有 Lambda 表示式的 引數(Parameter) 都改為 同一個引數(Parameter),再把內容(Body)組合起來

2. 驗證

假設我們有這樣的資料:

[
    {
        "Name": "安柏",
        "Age": 25,
        "Weapons": [
            {
                "Name": "普通弓",
                "GetTime": "2021-05-03T12:00:00+08:00",
                "Description": "入門級武器,從無到有"
            }
        ]
    },
    {
        "Name": "行秋",
        "Age": 18,
        "Weapons": [
            {
                "Name": "單手劍",
                "GetTime": "2021-05-22T11:00:00+08:00",
                "Description": "剛剛夠用的武器"
            }
        ]
    },
    {
        "Name": "可莉",
        "Age": 8,
        "Weapons": [
            {
                "Name": "嘟嘟可故事集",
                "GetTime": "2021-06-20T15:35:00+08:00",
                "Description": "特別合適的武器"
            },
            {
                "Name": "簡單的書",
                "GetTime": "2021-05-03T16:47:00+08:00",
                "Description": "贈送的武器"
            }
        ]
    }
]

(原來你也玩原船,,,哦不是,原神啊)。主要有 “角色”“年齡”(我瞎編的)、“武器”(半真半編的)這幾個欄位,其中 “武器”是單獨一個類,定義如下:

/// <summary>
/// 原神角色
/// </summary>
public class YuanshenRole
{
    /// <summary>
    /// 名字
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 年齡
    /// </summary>
    public int Age { get; set; }
    /// <summary>
    /// 所擁有的武器
    /// </summary>
    public List<Weapon> Weapons { get; set; }
}

/// <summary>
/// 武器
/// </summary>
public class Weapon
{
    /// <summary>
    /// 名稱
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 獲得的時間
    /// </summary>
    public DateTime GetTime { get; set; }
    /// <summary>
    /// 描述
    /// </summary>
    public string Description { get; set; }
}

我們要查詢 Age 為 8, Name 為“可莉”,Weapons中存在 “嘟嘟可故事集” 的角色,分成兩個表示式寫,並組合起來:

// 三個單獨的條件
Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
Expression<Func<YuanshenRole, bool>> filter1 = x => x.Name == 可莉";
Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");

// 組合起來
var finalFilter = CombineLambda(
                    new List<Expression<Func<YuanshenRole, bool>>> { filter, filter1, filter2 }, 
                    new List<Type> { typeof(YuanshenRole), typeof(Weapon) }
                ); 

CombineLambda 方法需要兩個引數,一個是 表示式的集合,另一個是 表示式中 Parameter 型別的集合。

踩坑過程

1. 只拼接Lambda表示式的Body,不改變其引數

說起拼接,我的第一想法是這樣的:

// 錯誤的寫法
Expression<Func<YuanshenRole, bool>> finalFitler = Expression.Lambda<Func<YuanshenRole, bool>>(
    Expression.AndAlso(filter.Body, filter1.Body),    // 即使取了表示式的 Body,意義也不對
    Expression.Parameter(typeof(YuanshenRole), "x")
);

報錯為:

從作用域“”引用了“UsageDemo.Program+YuanshenRole”型別的變數“x”,但該變數未定義”

在查詢資料的時候看到了一個比喻:每個 Lambda 表示式就像是一個獨輪車,把它們拼起來就相當於把兩個獨輪車拼成一個自行車。而上述的過程,我們看似把兩個輪子拆下來,安裝在了同一個車架子上,卻沒有改造兩個輪子讓其適應新的車架子(即,改變 “原有表示式 Body” 中引用 “原來Parameter” 的部分)

自然而然地,我們需要進行一番深入的改造。搜尋網上的資料後發現,可以通過繼承 ExpressionVisitor 類並覆寫其中的部分方法,如下:

class MyExpressionVisitor : ExpressionVisitor
{
    public ParameterExpression _Parameter0 { get; set; }
    public MyExpressionVisitor(ParameterExpression Parameter0)
    {
        _Parameter0 = Parameter0;
    }
    protected override Expression VisitParameter(ParameterExpression p)
    {
        return _Parameter0;
    }
    public override Expression Visit(Expression node)
    {
        return base.Visit(node);
    }
}

VisitParameter 方法在 Visit 時預設返回原 Parameter,通過覆寫它可以達到改造 Body 中引用 “原來Parameter” 的效果

ExpressionVisitor 中的 Visit 方法感覺與常規方法思路完全不同,先挖個坑,以後再研究

順理成章地,踩了第二個坑QAQ

2. 只傳入了 1 個 Parameter,而實際需要 2 個

根據踩坑1,寫法應該為:

Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;


ParameterExpression pe = Expression.Parameter(typeof(YuanshenRole), "x");
var visitor1 = new MyExpressionVisitor(pe);            
Expression bodyone1 = visitor1.Visit(filter.Body);    // 此處需要呼叫我們自定類的 Visit 方法改造原 Parameter
Expression bodytwo1 = visitor1.Visit(filter2.Body);
Expression<Func<YuanshenRole, bool>> finalFilter = 
    ExpressionLambda<Func<YuanshenRole, bool>>(Expression.AndAlso(bodyone1,bodytwo1), pe);
dataList = dataList.AsQueryable().Where(finalFilter).ToList();

結果報錯為:

沒有為型別“UsageDemo.Program+YuanshenRole”定義屬性“System.String Name””

發生甚麼事了,我們的 filter 也沒有獲取 YuanshenRole 的 Name 屬性啊。此處我確實愣了一下,然後發現其實 查詢的是 Weapon 型別的 Name,而傳入的是 YuanshenRole 型別的引數

因此在覆寫的 Visit 方法中不能只返回一個型別,而要根據實際情況返回。稍加封裝就有了文字最開始的程式碼。

private class CombineExpressionVisitor : ExpressionVisitor
{
    ......
    public CombineExpressionVisitor(List<ParameterExpression> list)
    {
        peList = list;
    }
    ......

這也是為什麼 Visit 類的建構函式要傳入 List<ParameterExpression> 的原因。

後記

最一開始我覺得合併兩個表示式應該是個很簡單的操作,可能一個方法就搞定了,沒想到他不講武德,讓我搞了這麼長時間。我大 E 了啊,沒有閃。希望這些程式碼以後耗子喂汁,不要再搞這樣的聰明,小聰明啊,謝謝朋友們!

參考

LINQ系列(7)——表示式樹之EXPRESSIONVISITOR

合併兩個 Lambda 表示式(此文中還介紹了通過 Invoke 方法來達到上述目的,但不適用於 IQueryable 型別的操作)

C#中合併兩個lambda表示式

C#中利用Expression表示式樹進行多個Lambda表示式合併

C# 知識回顧 - 表示式樹 Expression Trees