Выражения LINQ. Переменная "p" типа, на которую ссылается область, но не определена

Я строю LINQ-запрос динамически с помощью этого кода. Кажется, что это работает, но когда у меня есть несколько поисковых строк в моем поиске (поэтому, когда добавлено несколько выражений, я получаю следующую ошибку:

Переменная "p" типа, ссылающаяся на область видимости, но не определена **

Думаю, я могу только определить/использовать p один раз. Но, если это так, мне нужно немного изменить код. Может ли кто-нибудь указать мне в правильном направлении?

    if (searchStrings != null)
    {
        foreach (string searchString in searchStrings)
        {
            Expression<Func<Product, bool>> containsExpression = p => p.Name.Contains(searchString);
            filterExpressions.Add(containsExpression);
        }
    }

    Func<Expression, Expression, BinaryExpression>[] operators = new Func<Expression, Expression, BinaryExpression>[] { Expression.AndAlso };
    Expression<Func<Product, bool>> filters = this.CombinePredicates<Product>(filterExpressions, operators);

    IQueryable<Product> query = cachedProductList.AsQueryable().Where(filters);

    query.Take(itemLimit).ToList();  << **error when the query executes**


    public Expression<Func<T, bool>> CombinePredicates<T>(IList<Expression<Func<T, bool>>> predicateExpressions, Func<Expression, Expression, BinaryExpression> logicalFunction)
    {
        Expression<Func<T, bool>> filter = null;

        if (predicateExpressions.Count > 0)
        {
            Expression<Func<T, bool>> firstPredicate = predicateExpressions[0];
            Expression body = firstPredicate.Body;
            for (int i = 1; i < predicateExpressions.Count; i++)
            {
                body = logicalFunction(body, predicateExpressions[i].Body);
            }
            filter = Expression.Lambda<Func<T, bool>>(body, firstPredicate.Parameters);
        }

        return filter;
    }

Ответ 1

Упрощение, вот несколько строк, которые вы пытаетесь сделать (я использую строку вместо Product и т.д., Но идея такая же):

        Expression<Func<string, bool>> c1 = x => x.Contains("111");
        Expression<Func<string, bool>> c2 = y => y.Contains("222");
        var sum = Expression.AndAlso(c1.Body, c2.Body);
        var sumExpr = Expression.Lambda(sum, c1.Parameters);
        sumExpr.Compile(); // exception here

Обратите внимание, как я расширил ваш foreach на два выражения с помощью x и y - это точно так, как это выглядит для компилятора, это разные параметры.

Другими словами, вы пытаетесь сделать что-то вроде этого:

x => x.Contains("...") && y.Contains("...");

и компилятор задается вопросом, что это за переменная 'y'?

Чтобы исправить это, нам нужно использовать точно такой же параметр (не только имя, но и ссылку) для всех выражений. Мы можем исправить этот упрощенный код следующим образом:

        Expression<Func<string, bool>> c1 = x => x.Contains("111");
        Expression<Func<string, bool>> c2 = y => y.Contains("222");
        var sum = Expression.AndAlso(c1.Body, Expression.Invoke(c2, c1.Parameters[0])); // here is the magic
        var sumExpr = Expression.Lambda(sum, c1.Parameters);
        sumExpr.Compile(); //ok

Итак, исправление исходного кода было бы следующим:

internal static class Program
{
    public class Product
    {
        public string Name;
    }

    private static void Main(string[] args)
    {
        var searchStrings = new[] { "111", "222" };
        var cachedProductList = new List<Product>
        {
            new Product{Name = "111 should not match"},
            new Product{Name = "222 should not match"},
            new Product{Name = "111 222 should match"},
        };

        var filterExpressions = new List<Expression<Func<Product, bool>>>();
        foreach (string searchString in searchStrings)
        {
            Expression<Func<Product, bool>> containsExpression = x => x.Name.Contains(searchString); // NOT GOOD
            filterExpressions.Add(containsExpression);
        }

        var filters = CombinePredicates<Product>(filterExpressions, Expression.AndAlso);

        var query = cachedProductList.AsQueryable().Where(filters);

        var list = query.Take(10).ToList();
        foreach (var product in list)
        {
            Console.WriteLine(product.Name);
        }
    }

    public static Expression<Func<T, bool>> CombinePredicates<T>(IList<Expression<Func<T, bool>>> predicateExpressions, Func<Expression, Expression, BinaryExpression> logicalFunction)
    {
        Expression<Func<T, bool>> filter = null;

        if (predicateExpressions.Count > 0)
        {
            var firstPredicate = predicateExpressions[0];
            Expression body = firstPredicate.Body;
            for (int i = 1; i < predicateExpressions.Count; i++)
            {
                body = logicalFunction(body, Expression.Invoke(predicateExpressions[i], firstPredicate.Parameters));
            }
            filter = Expression.Lambda<Func<T, bool>>(body, firstPredicate.Parameters);
        }

        return filter;
    }
}

Но заметьте вывод:

222 should not match
111 222 should match

Не то, что вы можете ожидать.. Это результат использования searchString в foreach, который следует переписать следующим образом:

        ...
        foreach (string searchString in searchStrings)
        {
            var name = searchString;
            Expression<Func<Product, bool>> containsExpression = x => x.Name.Contains(name);
            filterExpressions.Add(containsExpression);
        }
        ...

И вот вывод:

111 222 should match

Ответ 2

ИМХО, нет необходимости составлять список:

var filterExpressions = new List<Expression<Func<Product, bool>>>()

Вы можете легко жить со следующим классом Visitor:

public class FilterConverter : IFilterConverterVisitor<Filter> {

    private LambdaExpression ConditionClausePredicate { get; set; }
    private ParameterExpression Parameter { get; set; }

    public void Visit(Filter filter) {

        if (filter == null) {
            return;
        }

        if (this.Parameter == null) {
            this.Parameter = Expression.Parameter(filter.BaseType, "x");
        }

        ConditionClausePredicate = And(filter);
    }

    public Delegate GetConditionClause() {

        if (ConditionClausePredicate != null) {

            return ConditionClausePredicate.Compile();
        }

        return null;
    }

    private LambdaExpression And(Filter filter) {

        if (filter.BaseType == null || string.IsNullOrWhiteSpace(filter.FlattenPropertyName)) {

            //Something is wrong, passing by current filter
            return ConditionClausePredicate;
        }

        var conditionType = filter.GetCondition();
        var propertyExpression = filter.BaseType.GetFlattenPropertyExpression(filter.FlattenPropertyName, this.Parameter);

        switch (conditionType) {

            case FilterCondition.Equal: {

                var matchValue = TypeDescriptor.GetConverter(propertyExpression.ReturnType).ConvertFromString(filter.Match);
                var propertyValue = Expression.Constant(matchValue, propertyExpression.ReturnType);
                var equalExpression = Expression.Equal(propertyExpression.Body, propertyValue);
                if (ConditionClausePredicate == null) {
                    ConditionClausePredicate = Expression.Lambda(equalExpression, this.Parameter);
                } else {
                    ConditionClausePredicate = Expression.Lambda(Expression.And(ConditionClausePredicate.Body, equalExpression), this.Parameter);
                }
                break;
            }
        // and so on...
    }
}

Код не оптимален, я знаю, я новичок и много всего, что нужно реализовать... Но этот материал действительно работает. Идея состоит в том, чтобы иметь единственный класс ParameterExpression per Visitor, а затем создавать выражения с использованием этого параметра. После этого просто конкатенируйте все выражения в одном предложении LambdaExpression и скомпилируйте для делегирования, когда это необходимо.