Объединение двух выражений (Expression ‹Func‹ T, bool ››)

У меня есть два выражения типа 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

person BjartN    schedule 19.01.2009    source источник


Ответы (8)


Что ж, вы можете использовать Expression.AndAlso / OrElse и т.д. для объединения логических выражений, но проблема в параметрах; вы работаете с одним и тем же ParameterExpression в expr1 и expr2? Если так, то проще:

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);
        }
    }
person Marc Gravell    schedule 19.01.2009
comment
Привет, Марк, я попробовал ваше первое предложение в первом блоке кода выше, но когда я передаю лямбда-выражение ‹func‹ T, bool ›› в результате метода Where, я получаю сообщение об ошибке, сообщающее, что параметр вне сфера? любая идея? ваше здоровье - person andy; 27.06.2009
comment
@Andy - да (см. Первое предложение) - вы получите это, если используете разные экземпляры параметров в двух версиях ... Я обновлю другой вариант (но который работает не для всех поставщиков; LINQ-to -Objects и LINQ-to-SQL будут в порядке, но EF не будет ...) - person Marc Gravell; 27.06.2009
comment
Ооо! Я уже рассмотрел это во второй версии ... это можно немного упростить, хотя (обновлю) - person Marc Gravell; 27.06.2009
comment
+1 обобщенная версия работает как шарм, я использовал И вместо andalso я думал, что linq to sql не поддерживает andalso? - person Maslow; 04.09.2009
comment
ты легенда. Я только что попробовал, и обобщенная версия работает отлично! (Я тестировал его с NH 3.0 alpha 2) - person dbones; 10.09.2010
comment
хм, получение вызова не поддерживается для EF4, есть ли версия, которая подойдет вам для этого marc? - person Maslow; 19.03.2011
comment
@Maslow - вот перезаписчик, который может встроить деревья для сохранения Invoke: stackoverflow.com/questions/1717444/ - person Marc Gravell; 20.03.2011
comment
@Marc - спасибо большое, я полагаю, что в коде есть и другие места, где это было необходимо, но пока это выглядит как просто цепочка. Где (cond1) .FirstOrDefault (cond2) работает в том месте, которое вернуло меня сюда, не уверен в масштабируемости или производительности. - person Maslow; 20.03.2011
comment
-1 для использования Expression.Invoke вместо использования посетителя дерева выражений для замены параметров. - person Aron; 13.08.2014
comment
@Aron теперь посмотри на дату: посетитель .NET framework (ExpressionVisitor) тогда не существовал; У меня есть связанный пример с stackoverflow из аналогичной даты, где он реализует посетителя вручную: это много кода. - person Marc Gravell; 13.08.2014
comment
@MarcGravell а ... Я не понимал, что посетитель - это новая вещь для .net 4. Вы не возражаете, если я обновлю ваш ответ для новых пользователей? - person Aron; 13.08.2014
comment
@Aron во что бы то ни стало, давай - person Marc Gravell; 13.08.2014
comment
@Aron Привет, я попробовал Ваше обновление с ExpressionVisitor и получил Expression of type 'System.Func2 [(TSource), System.Boolean] 'нельзя использовать для возвращаемого типа' System.Boolean`, что может быть причиной этого? - person Prokurors; 06.03.2015
comment
Спасибо @Marc, ваше решение очень помогло :) Мне это было нужно для условия OrElse, поэтому я изменил ваш код для своих нужд. еще раз спасибо :) - person Zain Shaikh; 26.11.2015
comment
привет @MarcGravell, как это сделать, когда мы используем два разных 'ParameterExpression', пожалуйста, это было бы действительно полезно для меня - person Nishanth Shaan; 28.06.2016
comment
@Nishanth просто используйте Dictionary<Expression,Expression> и немного TryGetValue, и он должен работать для любого количества свопов - person Marc Gravell; 28.06.2016
comment
Спасибо, @MarcGravell, но я следил за tuto codeproject .com / Tips / 582450 / Но что мне нужно, так это иметь возможность запрашивать и дочерние объекты ... возможно ли это? теперь я могу сделать db.Jobs.Include (j = ›j.SubCategory) .Where (MyExpressionBuilder.Build ‹Job› (фильтры)). ToList (); что я хочу сделать: db.Jobs.Include (j = ›j.SubCategory). .Where (MyExpressionBuilder.Build ‹Задание (фильтры) && MyExpressionBuilder.Build ‹Subcategory› (фильтры2)). ToList (); Пожалуйста, проконсультируйтесь по статье для получения подробной информации, пожалуйста, есть идеи? - person Nishanth Shaan; 04.07.2016
comment
@MarcGravell Замечательный фрагмент кода! Однако немного быстрее использовать expr1.Parameters [0] в качестве выражения параметра, тогда вам не нужно посещать левое выражение :-) - person Alexander Derck; 09.11.2016
comment
@MarkGravell, я использую ваше первое решение для объединения моих выражений, и все работает нормально даже в entityframework. Итак, каковы будут преимущества использования последнего решения? - person johnny 5; 03.08.2017
comment
@ johnny5 первое (более простое) решение должно работать, только если параметры в лямбда-телах совпадают. учитывая x => x > 5 и y => y < 100, вам нужно сделать z => z > 5 && z < 100, а не z => x > 5 && y < 100. если x и y уже каким-то образом являются одним и тем же параметром, то это не проблема. если это не так, то одно выражение необходимо переписать с параметром другого (или оба выражения переписать новым), что позволяет ReplaceExpressionVisitor. (обратите внимание, что параметры должны быть одинаковыми объектами, а не просто иметь одинаковые имена). - person Dave Cousineau; 25.01.2018
comment
@DaveCousineau Да, я так тяжело это понял, когда он вернулся, чтобы укусить меня. Но просто чтобы добавить к этому событию точки, если у вас есть x => x > 5 и отдельное выражение x => x < 100, эти x являются локальными для выражения, и если вы попытаетесь их использовать, даже вы все равно получите исключение - person johnny 5; 25.01.2018
comment
Большое тебе спасибо - person Andreas; 22.01.2019
comment
Обратите внимание, что вы можете несколько оптимизировать это: вам не нужно переписывать и expr1, и expr2, но вместо этого вы можете переписать просто expr2 и заменить expr2.Parameters[0] на expr1.Parameters[0], оставив expr1 как есть - person canton7; 22.01.2020
comment
@MarcGravell Спасибо за это. Вы знаете, безопасен ли первый блок кода (2 строки) для EF Core? Во время компиляции он сообщает, что его нельзя перевести, но я не понимаю, почему. - person user3071284; 07.07.2020
comment
@ user3071284, потому что синтаксический анализ деревьев выражений сложен, поэтому иногда нам нужно помочь им; попробуйте версию выражения посетителя внизу - person Marc Gravell; 07.07.2020
comment
отличное решение, большое спасибо! - person Nieksa; 03.03.2021

Вы можете использовать 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);
    }   
}

И служебный класс для замены параметров в лямбда:

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;
        }
    }
person Adam Tegen    schedule 19.09.2012
comment
Это решение было единственным, которое позволило мне иметь x = ›x.Property == Value в сочетании с arg =› arg.Property2 == Value. Основные реквизиты, немного краткие и запутанные, но они работают, поэтому я не собираюсь жаловаться. Престижность Адама :-) - person VulgarBinary; 13.12.2012
comment
Это отличное решение. - person Aaron Stainback; 14.05.2014
comment
Адам, это решило очень неприятную проблему, с которой я столкнулся при использовании поставщика Linq клиентской объектной модели SharePoint - спасибо за публикацию. - person Christopher McAtackney; 09.07.2014
comment
Это сработало для меня! Я искал множество решений, а также построитель предикатов, и до этого ничего не работало. Спасибо! - person tokyo0709; 01.08.2016
comment
Это замечательный фрагмент кода. Я не нашел, где подправить код, копипастить и все :) - person Tolga Evcimen; 16.02.2017

Если ваш провайдер не поддерживает 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" }));
}
person Francis    schedule 15.12.2011
comment
Это решило мою конкретную проблему, когда другое решение привело к тому же исключению. Спасибо. - person Shaun Wilson; 08.03.2013
comment
Это отличное решение. - person Aaron Stainback; 14.05.2014

Здесь нет ничего нового, но женился на этом ответе с этот ответ и немного отредактировал его, чтобы даже я понял, что происходит:

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);
        }
    }
}
person Dejan    schedule 05.02.2020
comment
Мне было трудно понять концепцию, и ваше объединение пары других ответов помогло мне понять ее. Спасибо! - person Kevin M. Lapio; 17.04.2020

Мне нужно было добиться тех же результатов, но с использованием чего-то более общего (поскольку тип не был известен). Благодаря ответу Марка я наконец понял, чего пытался достичь:

    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);
    }
person VorTechS    schedule 01.06.2017

Я предлагаю еще одно улучшение решений PredicateBuilder и ExpressionVisitor. Я назвал его UnifyParametersByName, и вы можете найти его в моей библиотеке MIT: LinqExprHelper. Он позволяет комбинировать произвольные лямбда-выражения. Обычно вопросы задают о выражении предиката, но эта идея распространяется и на выражения проекции.

В следующем коде используется метод ExprAdres, который создает сложное параметризованное выражение с использованием встроенной лямбда-выражения. Это сложное выражение кодируется только один раз, а затем используется повторно благодаря мини-библиотеке 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();
}

Я пытался выполнить параметризованные запросы без необходимости копировать-вставки и с возможностью использовать встроенные лямбды, которые так красивы. Без всех этих вспомогательных выражений я был бы вынужден создать весь запрос за один раз.

person Jarekczek    schedule 16.10.2016

Я объединил здесь несколько красивых ответов, чтобы можно было легко поддерживать больше операторов Expression.

Это основано на ответе @Dejan, но теперь довольно легко добавить ИЛИ. Я решил не делать функцию 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);

(Это пример, основанный на моем реальном коде, но читается как псевдокод)

person Nick N.    schedule 23.02.2021

Я думаю, это нормально работает, не так ли?

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));
person Céline    schedule 13.11.2015
comment
это нельзя использовать, например, в Linq to SQL - person Romain Vergnory; 12.05.2016