组合两个表达式(表达式<Func<T, bool>>)

问题描述 投票:0回答:10

我有两个类型为

Expression<Func<T, bool>>
的表达式,我想对它们进行 OR、AND 或 NOT 运算并得到一个相同类型的新表达式

Expression<Func<T, bool>> expr1;
Expression<Func<T, bool>> expr2;

...

//how to do this (the code below will obviously not work)
Expression<Func<T, bool>> andExpression = expr AND expr2
c# linq lambda expression
10个回答
455
投票

嗯,你可以使用

Expression.AndAlso
/
OrElse
等来组合逻辑表达式,但问题是参数;您在 expr1 和 expr2 中使用相同的
ParameterExpression
吗?如果是这样,那就更容易了:

var body = Expression.AndAlso(expr1.Body, expr2.Body);
var lambda = Expression.Lambda<Func<T,bool>>(body, expr1.Parameters[0]);

这对于否定单个操作也很有效:

static Expression<Func<T, bool>> Not<T>(
    this Expression<Func<T, bool>> expr)
{
    return Expression.Lambda<Func<T, bool>>(
        Expression.Not(expr.Body), expr.Parameters[0]);
}

否则,根据 LINQ 提供商的不同,您也许可以将它们与

Invoke
:

结合使用
// OrElse is very similar...
static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> left,
    Expression<Func<T, bool>> right)
{
    var param = Expression.Parameter(typeof(T), "x");
    var body = Expression.AndAlso(
            Expression.Invoke(left, param),
            Expression.Invoke(right, param)
        );
    var lambda = Expression.Lambda<Func<T, bool>>(body, param);
    return lambda;
}

在某个地方,我有一些代码可以重写表达式树替换节点,以消除对

Invoke
的需要,但它相当冗长(而且我不记得我把它放在哪里了......)


选择最简单路线的通用版本:

static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> expr1,
    Expression<Func<T, bool>> expr2)
{
    // need to detect whether they use the same
    // parameter instance; if not, they need fixing
    ParameterExpression param = expr1.Parameters[0];
    if (ReferenceEquals(param, expr2.Parameters[0]))
    {
        // simple version
        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(expr1.Body, expr2.Body), param);
    }
    // otherwise, keep expr1 "as is" and invoke expr2
    return Expression.Lambda<Func<T, bool>>(
        Expression.AndAlso(
            expr1.Body,
            Expression.Invoke(expr2, param)), param);
}

从 .NET 4.0 开始,有了

ExpressionVisitor
类,它允许您构建 EF 安全的表达式。

    public static Expression<Func<T, bool>> AndAlso<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var parameter = Expression.Parameter(typeof (T));

        var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
        var left = leftVisitor.Visit(expr1.Body);

        var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
        var right = rightVisitor.Visit(expr2.Body);

        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(left, right), parameter);
    }



    private class ReplaceExpressionVisitor
        : ExpressionVisitor
    {
        private readonly Expression _oldValue;
        private readonly Expression _newValue;

        public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
        {
            _oldValue = oldValue;
            _newValue = newValue;
        }

        public override Expression Visit(Expression node)
        {
            if (node == _oldValue)
                return _newValue;
            return base.Visit(node);
        }
    }

92
投票

您可以使用Expression.AndAlso / OrElse来组合逻辑表达式,但您必须确保ParameterExpressions相同。

我在使用 EF 和 PredicateBuilder 时遇到了问题,所以我自己制作了一个,而不求助于 Invoke,我可以这样使用:

var filterC = filterA.And(filterb);

我的 PredicateBuilder 的源代码:

public static class PredicateBuilder {

    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }   
}

以及用于替换 lambda 中的参数的实用程序类:

internal class SubstExpressionVisitor : System.Linq.Expressions.ExpressionVisitor {
        public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();

        protected override Expression VisitParameter(ParameterExpression node) {
            Expression newValue;
            if (subst.TryGetValue(node, out newValue)) {
                return newValue;
            }
            return node;
        }
    }

25
投票

如果您的提供程序不支持 Invoke,并且您需要组合两个表达式,您可以使用 ExpressionVisitor 将第二个表达式中的参数替换为第一个表达式中的参数。

class ParameterUpdateVisitor : ExpressionVisitor
{
    private ParameterExpression _oldParameter;
    private ParameterExpression _newParameter;

    public ParameterUpdateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (object.ReferenceEquals(node, _oldParameter))
            return _newParameter;

        return base.VisitParameter(node);
    }
}

static Expression<Func<T, bool>> UpdateParameter<T>(
    Expression<Func<T, bool>> expr,
    ParameterExpression newParameter)
{
    var visitor = new ParameterUpdateVisitor(expr.Parameters[0], newParameter);
    var body = visitor.Visit(expr.Body);

    return Expression.Lambda<Func<T, bool>>(body, newParameter);
}

[TestMethod]
public void ExpressionText()
{
    string text = "test";

    Expression<Func<Coco, bool>> expr1 = p => p.Item1.Contains(text);
    Expression<Func<Coco, bool>> expr2 = q => q.Item2.Contains(text);
    Expression<Func<Coco, bool>> expr3 = UpdateParameter(expr2, expr1.Parameters[0]);

    var expr4 = Expression.Lambda<Func<Recording, bool>>(
        Expression.OrElse(expr1.Body, expr3.Body), expr1.Parameters[0]);

    var func = expr4.Compile();

    Assert.IsTrue(func(new Coco { Item1 = "caca", Item2 = "test pipi" }));
}

15
投票

这里没有什么新鲜事,但与这个答案结婚了这个答案并稍微重构了它,以便我也明白发生了什么:

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        ParameterExpression parameter1 = expr1.Parameters[0];
        var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1);
        var body2WithParam1 = visitor.Visit(expr2.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, body2WithParam1), parameter1);
    }

    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private ParameterExpression _oldParameter;
        private ParameterExpression _newParameter;

        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (ReferenceEquals(node, _oldParameter))
                return _newParameter;

            return base.VisitParameter(node);
        }
    }
}

12
投票

我在这里组合了一些漂亮的答案,使其成为可能轻松支持更多表达式运算符

这是基于@Dejan的答案,但是现在添加OR也很容易。我选择不公开

Combine
函数,但您可以这样做以更加灵活。

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> leftExpression,
        Expression<Func<T, bool>> rightExpression) =>
        Combine(leftExpression, rightExpression, Expression.AndAlso);

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> leftExpression,
        Expression<Func<T, bool>> rightExpression) =>
        Combine(leftExpression, rightExpression, Expression.Or);

    public static Expression<Func<T, bool>> Combine<T>(Expression<Func<T, bool>> leftExpression, Expression<Func<T, bool>> rightExpression, Func<Expression, Expression, BinaryExpression> combineOperator)
    {
        var leftParameter = leftExpression.Parameters[0];
        var rightParameter = rightExpression.Parameters[0];

        var visitor = new ReplaceParameterVisitor(rightParameter, leftParameter);

        var leftBody = leftExpression.Body;
        var rightBody = visitor.Visit(rightExpression.Body);

        return Expression.Lambda<Func<T, bool>>(combineOperator(leftBody, rightBody), leftParameter);
    }

    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParameter;
        private readonly ParameterExpression _newParameter;

        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            return ReferenceEquals(node, _oldParameter) ? _newParameter : base.VisitParameter(node);
        }
    }
}

用法没有改变,还是这样:

Expression<Func<Result, bool>> noFilterExpression = item => filters == null;

Expression<Func<Result, bool>> laptopFilterExpression = item => item.x == ...
Expression<Func<Result, bool>> dateFilterExpression = item => item.y == ...

var combinedFilterExpression = noFilterExpression.Or(laptopFilterExpression.AndAlso(dateFilterExpression));
    
efQuery.Where(combinedFilterExpression);

(这是基于我的实际代码的示例,但将其视为伪代码)


5
投票
using System;
using System.Linq.Expressions;

namespace Extensions
{


    public class Example
    {
        //How to use it
        public static void Main()
        {
            Expression<Func<string, bool>> expression1 = exp => true;
            Expression<Func<string, bool>> expression2 = exp => false;

            Expression<Func<string, bool>> expression3 = ExpressionExtensions.AndAlso(expression1, expression2);
            Expression<Func<string, bool>> expression4 = ExpressionExtensions.OrElse(expression1, expression2);

            Expression<Func<string, bool>> expression = ExpressionExtensions.AndAlso(expression3, expression4);
        }
    }


    public static class ExpressionExtensions
    {
        public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
        {
            ParameterExpression parameter1 = expr1.Parameters[0];
            var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1);
            var body2WithParam1 = visitor.Visit(expr2.Body);
            return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, body2WithParam1), parameter1);
        }

        public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
        {
            ParameterExpression parameter1 = expr1.Parameters[0];
            var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1);
            var body2WithParam1 = visitor.Visit(expr2.Body);
            return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, body2WithParam1), parameter1);
        }

        private class ReplaceParameterVisitor : ExpressionVisitor
        {
            private readonly ParameterExpression _oldParameter;
            private readonly ParameterExpression _newParameter;

            public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
            {
                _oldParameter = oldParameter;
                _newParameter = newParameter;
            }

            protected override Expression VisitParameter(ParameterExpression node)
            {
                if (ReferenceEquals(node, _oldParameter))
                    return _newParameter;

                return base.VisitParameter(node);
            }
        }
    }
}

4
投票

我需要实现相同的结果,但使用更通用的东西(因为类型未知)。 感谢马克的回答,我终于弄清楚了我想要实现的目标:

    public static LambdaExpression CombineOr(Type sourceType, LambdaExpression exp, LambdaExpression newExp) 
    {
        var parameter = Expression.Parameter(sourceType);

        var leftVisitor = new ReplaceExpressionVisitor(exp.Parameters[0], parameter);
        var left = leftVisitor.Visit(exp.Body);

        var rightVisitor = new ReplaceExpressionVisitor(newExp.Parameters[0], parameter);
        var right = rightVisitor.Visit(newExp.Body);

        var delegateType = typeof(Func<,>).MakeGenericType(sourceType, typeof(bool));
        return Expression.Lambda(delegateType, Expression.Or(left, right), parameter);
    }

3
投票

我建议对 PredicateBuilder

ExpressionVisitor
解决方案再进行一项改进。我将其称为
UnifyParametersByName
,您可以在我的 MIT 图书馆中找到它:LinqExprHelper。它允许组合任意 lambda 表达式。通常,问题是关于谓词表达式的,但这个想法也扩展到投影表达式。

以下代码采用方法

ExprAdres
,它使用内联 lambda 创建一个复杂的参数化表达式。这个复杂的表达式只需编码一次,然后就可以重复使用,这要归功于
LinqExprHelper
迷你库。

public IQueryable<UbezpExt> UbezpFull
{
    get
    {
        System.Linq.Expressions.Expression<
            Func<UBEZPIECZONY, UBEZP_ADRES, UBEZP_ADRES, UbezpExt>> expr =
            (u, parAdrM, parAdrZ) => new UbezpExt
            {
                Ub = u,
                AdrM = parAdrM,
                AdrZ = parAdrZ,
            };

        // From here an expression builder ExprAdres is called.
        var expr2 = expr
            .ReplacePar("parAdrM", ExprAdres("M").Body)
            .ReplacePar("parAdrZ", ExprAdres("Z").Body);
        return UBEZPIECZONY.Select((Expression<Func<UBEZPIECZONY, UbezpExt>>)expr2);
    }
}

这是子表达式构建代码:

public static Expression<Func<UBEZPIECZONY, UBEZP_ADRES>> ExprAdres(string sTyp)
{
    return u => u.UBEZP_ADRES.Where(a => a.TYP_ADRESU == sTyp)
        .OrderByDescending(a => a.DATAOD).FirstOrDefault();
}

我试图实现的是执行参数化查询,无需复制粘贴,并且能够使用内联 lambda,这非常漂亮。如果没有所有这些辅助表达式的东西,我将被迫一次性创建整个查询。


2
投票

通过一种方法和小枚举来标识表达式操作类型的另一种解决方案(没有 ExpressionVisitor)

private static Expression<Func<T, bool>> BindExpressions<T>(ExpressionOperationType operationType, Expression<Func<T, bool>>[] expressionPredicates)
{
    var filterExpressionPredicate = expressionPredicates.FirstOrDefault() ?? (x => false);

    if (expressionPredicates.Length > 1)
        for (int i = 1; i < expressionPredicates.Length; i++)
        {
            var expressionBody = Expression.Invoke(expressionPredicates[i], filterExpressionPredicate?.Parameters);
            var handledExpressionUnits = operationType switch
            {
                ExpressionOperationType.AndAlso => Expression.AndAlso(filterExpressionPredicate.Body, expressionBody),
                _ => Expression.OrElse(filterExpressionPredicate.Body, expressionBody),
            };

            filterExpressionPredicate = Expression.Lambda<Func<T, bool>>(handledExpressionUnits, filterExpressionPredicate.Parameters);
        }

    return filterExpressionPredicate;
}
        
enum ExpressionOperationType
{
    AndAlso = 0,
    OrElse = 1
}

例如: 我们有一个模型 AuditLog

public class AuditLog
{
    public Guid Id { get; set; }

    public string OldValues { get; set; }

    public string NewValues { get; set; }

    public DateTime Timestamp { get; set; }
}

我们想要构建特定的查询:在日期(时间戳)范围内搜索所有包含关键字“橙子”,“汽车”,“鸟”的审计记录

public IQueryable<AuditLog> BuildQuery()
{
    var query = _context.AuditLogs.AsNoTracking();

    var commonFilterList = new List<Expression<Func<AuditLog, bool>>>();
    commonFilterList.Add(x => x.Timestamp >= DateTime.Now);
    commonFilterList.Add(x => x.Timestamp <= DateTime.Now.AddDays(1));
    
    //real world such simple filter case I would use way like:
    //query = query
    //    .Where(x => x.Timestamp >= DateTime.Now)
    //    .Where(x => x.Timestamp <= DateTime.Now.AddDays(1));
    //but this point we keep the example

    //using AndAlso
    query = query.Where(BindExpressions(ExpressionOperationType.AndAlso, commonFilterList.ToArray()));

    //at this point we look at more useful example of using BindExpressions implementation via OrElse expression operation type
    var specificFilterList = new List<Expression<Func<AuditLog, bool>>>();

    var keyWordsToSearch = new List<string>() { "oranges", "cars", "birds" };
    
    foreach (var keyWord in keyWordsToSearch)
    {
        //real world we would to use EF.Functions.Contains / EF.Functions.FreeText statements <- but it is question another scope
        specificFilterList.Add(x => EF.Functions.Like(x.NewValues, $"%{keyWord}%"));
        specificFilterList.Add(x => EF.Functions.Like(x.OldValues, $"%{keyWord}%"));
    }

    //using OrElse
    query = query.Where(BindExpressions(ExpressionOperationType.OrElse, specificFilterList.ToArray()));

    //as result we get commonFilterList AND specificFilterList
    return query;
}

-12
投票

我认为这很好用,不是吗?

Func<T, bool> expr1 = (x => x.Att1 == "a");
Func<T, bool> expr2 = (x => x.Att2 == "b");
Func<T, bool> expr1ANDexpr2 = (x => expr1(x) && expr2(x));
Func<T, bool> expr1ORexpr2 = (x => expr1(x) || expr2(x));
Func<T, bool> NOTexpr1 = (x => !expr1(x));
© www.soinside.com 2019 - 2024. All rights reserved.