关于c#:如何完成复杂查询的动态构建

5次阅读

共计 9491 个字符,预计需要花费 24 分钟才能阅读完成。

有的时候,你须要动静构建一个比较复杂的查问条件,传入数据库中进行查问。而条件自身可能来自前端申请或者配置文件。那么这个时候,表达式树,就能够帮忙到你。本文咱们将通过几个简短的示例来理解如何实现这些操作。

微软 MVP 实验室研究员

你也可能接到过这些需要:

(图片从模型进行查问)

(基于配置查问)

明天咱们看看表达式树如何实现这些需要。

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;
    }
}

总结

如果逻辑关系更简单,有多层嵌套像树形一样,比拟办法也很多花色,甚至蕴含办法,怎么办?
能够参考以下示例:

  • https://github.com/newbe36524…

如果你对此内容感兴趣,还能够浏览我之前录制的视频进行进一步理解:

戏精分享 C# 表达式树,第一季

  • https://www.bilibili.com/vide…

戏精分享 C# 表达式树,第二季

  • https://www.bilibili.com/vide…

你也能够参阅之前一篇入门:

《只有十步,你就能够利用表达式树来优化动静调用》

  • https://www.newbe.pro/Newbe.C…

或者看 MSDN 文档,我感觉你也能够有所播种:

  • https://docs.microsoft.com/do…

这篇相干的代码,能够通过以下地址失去:

  • https://github.com/newbe36524…

如果你感觉本文不错,记得珍藏、点赞、评论、转发。通知我还想晓得点什么哟!

微软最有价值专家(MVP)

微软最有价值专家是微软公司授予第三方技术专业人士的一个寰球奖项。28 年来,世界各地的技术社区领导者,因其在线上和线下的技术社区中分享专业知识和教训而取得此奖项。

MVP 是通过严格筛选的专家团队,他们代表着技术最精湛且最具智慧的人,是对社区投入极大的激情并乐于助人的专家。MVP 致力于通过演讲、论坛问答、创立网站、撰写博客、分享视频、开源我的项目、组织会议等形式来帮忙别人,并最大水平地帮忙微软技术社区用户应用 Microsoft 技术。
更多详情请登录官方网站:
https://mvp.microsoft.com/zh-cn


欢送关注微软中国 MSDN 订阅号,获取更多最新公布!

正文完
 0