如何完成複雜查詢的動態構建?

微軟技術棧發表於2021-11-17

有的時候,你需要動態構建一個比較複雜的查詢條件,傳入資料庫中進行查詢。而條件本身可能來自前端請求或者配置檔案。那麼這個時候,表示式樹,就可以幫助到你。本文我們將通過幾個簡短的示例來了解如何完成這些操作。

微軟MVP實驗室研究員
image.png

你也可能接到過這些需求:
1bb0bef8daa3058cb1725db92266cbf.jpg

(圖片從模型進行查詢)
a863646e7b66db919aaca61fb64481e9.jpg
(基於配置查詢)

今天我們看看錶達式樹如何實現這些需求。

Where當中可以傳入固定的條件

以下是一個簡單的單元測試用例。接下來,我們將這個測試用例改的面目全非。

[Test]
public void Normal()
{
    var re = Enumerable.Range(0, 10).AsQueryable() // 0-9
        .Where(x => x >= 1 && x < 5).ToList(); // 1 2 3 4
    var expectation = Enumerable.Range(1, 4); // 1 2 3 4
    re.Should().BeEquivalentTo(expectation);
}

Queryable中的Where就是一種表示式樹

由於是 Queryable 的關係,所以Where當中的其實是一個表示式,那麼我們把它單獨定義出來,順便水一下文章的長度。

[Test]
public void Expression00()
{
    Expression<Func<int, bool>> filter = x => x >= 1 && x < 5;
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);
}

表示式可以通過Lambda隱式轉換

Expression 右側是一個 Lambda ,所以可以捕獲上下文中的變數。
這樣你便可以把 minValue 和 maxValue 單獨定義出來。
於是乎你可以從其他地方來獲取 minValue 和 maxValue 來改變 filter。

[Test]
public void Expression01()
{
    var minValue = 1;
    var maxValue = 5;
    Expression<Func<int, bool>> filter = x => x >= minValue && x < maxValue;
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);
}

可以使用方法建立表示式

那既然這樣,我們也可以使用一個方法來建立 Expression。
這個方法,實際上就可以認為是這個 Expression 的工廠方法。

[Test]
public void Expression02()
{
    var filter = CreateFilter(1, 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateFilter(int minValue, int maxValue)
    {
        return x => x >= minValue && x < maxValue;
    }
}

通過Func可以更加靈活的組合條件

那可以使用 minValue 和 maxValue 作為引數來製作工廠方法,那麼用委託當然也可以。
於是,我們可以把左邊和右邊分別定義成兩個 Func,從而由外部來決定左右具體的比較方式。

[Test]
public void Expression03()
{
    var filter = CreateFilter(x => x >= 1, x => x < 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateFilter(Func<int, bool> leftFunc, Func<int, bool> rightFunc)
    {
        return x => leftFunc.Invoke(x) && rightFunc.Invoke(x);
    }
}

也可以手動構建表示式

實際上,左右兩個不僅僅是兩個Func,其實也可以直接是兩個表示式。
不過稍微有點不同的是,表示式的合併需要用 Expression 型別中的相關方法建立。
我們可以發現,呼叫的地方這次其實沒有任何改變,因為 Lambda 既可以隱式轉換為 Func 也可以隱式轉換為 Expression。
每個方法的意思可以從註釋中看出。

[Test]
public void Expression04()
{
    var filter = CreateFilter(x => x >= 1, x => x < 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateFilter(Expression<Func<int, bool>> leftFunc,
        Expression<Func<int, bool>> rightFunc)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        // (a => leftFunc(a))(x)
        var leftExp = Expression.Invoke(leftFunc, pExp);
        // (a => rightFunc(a))(x)
        var rightExp = Expression.Invoke(rightFunc, pExp);
        // (a => leftFunc(a))(x) && (a => rightFunc(a))(x)
        var bodyExp = Expression.AndAlso(leftExp, rightExp);
        // x => (a => leftFunc(a))(x) && (a => rightFunc(a))(x)
        var resultExp = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        return resultExp;
    }
}

引入表示式的解構,使其更加簡單

但是,上面的方法,其實可以再優化一下。避免對左右表示式的直接呼叫。
使用一個叫做 Unwrap 的方法,可以將 Lambda Expression 解構成只包含 Body 部分的表示式。
這是一個自定義的擴充套件方法,你可以通過 ObjectVisitor 來引入這個方法。
限於篇幅,我們此處不能展開談 Unwrap 的實現。我們只需要關注和前一個示例中註釋的不同即可。

ObjectVisitor:https://github.com/newbe36524...
[Test]
public void Expression05()
{
    var filter = CreateFilter(x => x >= 1, x => x < 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateFilter(Expression<Func<int, bool>> leftFunc,
        Expression<Func<int, bool>> rightFunc)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        // leftFunc(x)
        var leftExp = leftFunc.Unwrap(pExp);
        // rightFunc(x)
        var rightExp = rightFunc.Unwrap(pExp);
        // leftFunc(x) && rightFunc(x)
        var bodyExp = Expression.AndAlso(leftExp, rightExp);
        // x => leftFunc(x) && rightFunc(x)
        var resultExp = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        return resultExp;
    }
}

可以拼接更多的表示式

我們可以再優化以下,把 CreateFilter 方法擴充套件為支援多個子表示式和可自定義子表示式的連線方式。
於是,我們就可以得到一個 JoinSubFilters 方法。

[Test]
public void Expression06()
{
    var filter = JoinSubFilters(Expression.AndAlso, x => x >= 1, x => x < 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,
        params Expression<Func<int, bool>>[] subFilters)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        var result = subFilters[0];
        foreach (var sub in subFilters[1..])
        {
            var leftExp = result.Unwrap(pExp);
            var rightExp = sub.Unwrap(pExp);
            var bodyExp = expJoiner(leftExp, rightExp);

            result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        }

        return result;
    }
}

使用工廠方法來代替固定的子表示式

有了前面的經驗,我們知道。其實x => x >= 1這個表示式可以通過一個工廠方法來建。
所以,我們使用一個 CreateMinValueFilter 來建立這個表示式。

[Test]
public void Expression07()
{
    var filter = JoinSubFilters(Expression.AndAlso,
        CreateMinValueFilter(1),
        x => x < 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateMinValueFilter(int minValue)
    {
        return x => x >= minValue;
    }

    Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,
        params Expression<Func<int, bool>>[] subFilters)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        var result = subFilters[0];
        foreach (var sub in subFilters[1..])
        {
            var leftExp = result.Unwrap(pExp);
            var rightExp = sub.Unwrap(pExp);
            var bodyExp = expJoiner(leftExp, rightExp);

            result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        }

        return result;
    }
}

工廠方法內部也可以使用Expression手動建立

當然,可以只使用 Expression 相關的方法來建立x => x >= 1

[Test]
public void Expression08()
{
    var filter = JoinSubFilters(Expression.AndAlso,
        CreateMinValueFilter(1),
        x => x < 5);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateMinValueFilter(int minValue)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        // minValue
        var rightExp = Expression.Constant(minValue);
        // x >= minValue
        var bodyExp = Expression.GreaterThanOrEqual(pExp, rightExp);
        var result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        return result;
    }

    Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,
        params Expression<Func<int, bool>>[] subFilters)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        var result = subFilters[0];
        foreach (var sub in subFilters[1..])
        {
            var leftExp = result.Unwrap(pExp);
            var rightExp = sub.Unwrap(pExp);
            var bodyExp = expJoiner(leftExp, rightExp);

            result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        }

        return result;
    }
}

同理,子表示式都可以如此建立

那既然都用了 Expression 來建立子表示式了,那就乾脆再做一點點改進,把x => x < 5也做成從工廠方法獲取。

[Test]
public void Expression09()
{
    var filter = JoinSubFilters(Expression.AndAlso,
        CreateValueCompareFilter(Expression.GreaterThanOrEqual, 1),
        CreateValueCompareFilter(Expression.LessThan, 5));
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Expression<Func<int, bool>> CreateValueCompareFilter(Func<Expression, Expression, Expression> comparerFunc,
        int rightValue)
    {
        var pExp = Expression.Parameter(typeof(int), "x");
        var rightExp = Expression.Constant(rightValue);
        var bodyExp = comparerFunc(pExp, rightExp);
        var result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        return result;
    }

    Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,
        params Expression<Func<int, bool>>[] subFilters)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        var result = subFilters[0];
        foreach (var sub in subFilters[1..])
        {
            var leftExp = result.Unwrap(pExp);
            var rightExp = sub.Unwrap(pExp);
            var bodyExp = expJoiner(leftExp, rightExp);

            result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        }

        return result;
    }
}

加入一點點配置,就完成了

最後,我們在把子表示式的建立通過一點點小技巧。通過外部引數來決定。就基本完成了一個多 And 的值比較查詢條件的動態構建。

[Test]
public void Expression10()
{
    var config = new Dictionary<string, int>
    {
        { ">=", 1 },
        { "<", 5 }
    };
    var subFilters = config.Select(x => CreateValueCompareFilter(MapConfig(x.Key), x.Value)).ToArray();
    var filter = JoinSubFilters(Expression.AndAlso, subFilters);
    var re = Enumerable.Range(0, 10).AsQueryable()
        .Where(filter).ToList();
    var expectation = Enumerable.Range(1, 4);
    re.Should().BeEquivalentTo(expectation);

    Func<Expression, Expression, Expression> MapConfig(string op)
    {
        return op switch
        {
            ">=" => Expression.GreaterThanOrEqual,
            "<" => Expression.LessThan,
            _ => throw new ArgumentOutOfRangeException(nameof(op))
        };
    }

    Expression<Func<int, bool>> CreateValueCompareFilter(Func<Expression, Expression, Expression> comparerFunc,
        int rightValue)
    {
        var pExp = Expression.Parameter(typeof(int), "x");
        var rightExp = Expression.Constant(rightValue);
        var bodyExp = comparerFunc(pExp, rightExp);
        var result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        return result;
    }

    Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,
        params Expression<Func<int, bool>>[] subFilters)
    {
        // x
        var pExp = Expression.Parameter(typeof(int), "x");
        var result = subFilters[0];
        foreach (var sub in subFilters[1..])
        {
            var leftExp = result.Unwrap(pExp);
            var rightExp = sub.Unwrap(pExp);
            var bodyExp = expJoiner(leftExp, rightExp);

            result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);
        }

        return result;
    }
}

總結

如果邏輯關係更復雜,有多層巢狀像樹形一樣,比較方法也很多花樣,甚至包含方法,怎麼辦?
可以參考以下示例:

如果你對此內容感興趣,還可以瀏覽我之前錄製的視訊進行進一步瞭解:

戲精分享 C#表示式樹,第一季

戲精分享 C#表示式樹,第二季

你也可以參閱之前一篇入門:

《只要十步,你就可以應用表示式樹來優化動態呼叫》

或者看MSDN文件,我覺得你也可以有所收穫:

這篇相關的程式碼,可以通過以下地址得到:

如果你覺得本文不錯,記得收藏、點贊、評論、轉發。告訴我還想知道點什麼喲!

微軟最有價值專家(MVP)

bc93fde364ea9dd3d9106b58e805b770.png

微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。28年來,世界各地的技術社群領導者,因其線上上和線下的技術社群中分享專業知識和經驗而獲得此獎項。

MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社群投入極大的熱情並樂於助人的專家。MVP致力於通過演講、論壇問答、建立網站、撰寫部落格、分享視訊、開源專案、組織會議等方式來幫助他人,並最大程度地幫助微軟技術社群使用者使用Microsoft技術。
更多詳情請登入官方網站:
https://mvp.microsoft.com/zh-cn


歡迎關注微軟中國MSDN訂閱號,獲取更多最新發布!
qrcode_for_gh_14ae6a09f046_258 (1).jpg

相關文章