Создание пользовательских выражений для Entity Framework (LINQ) - PullRequest
0 голосов
/ 19 декабря 2018

У меня есть следующий метод для создания некоторых пользовательских запросов EF для поддержки текстового фильтра, который очень близок к работе, но у меня возникла проблема с левой стороной собранного выражения.Когда я использую «Expression.Invoke» (первая строка тела метода), я получаю исключение, которое The LINQ expression node type 'Invoke' is not supported in LINQ to Entities., что имеет смысл для меня (я концептуально понимаю, что происходит в переводе LINQ => SQL).Поэтому я решил, что левой стороне выражения должно быть что-то более похожее на правую сторону (то есть с использованием Expression.Constant), где выполняется вся «предварительная обработка», чтобы LINQ to Entities знал, как создать левую часть выражения.

Но когда я использую 2-ю строку (Expression.Property), я получаю исключение:

Instance property 'PropertyName' is not defined for type System.Func2[Proj.EntityFramework.DomainObject,System.Decimal]'

Что я понимаю .... гораздо меньше.

Пример вызоварассматриваемый метод:

return context.DomainObjects.Where(BuildExpression(l => l.PropertyName, "<200"));

Итак, я примерно понимаю, что неправильно строю выражение и пытаюсь извлечь имя свойства из предоставленного выражения, а не того, что EF необходимо для компиляции оператора SQL, ноВ этот момент я немного растерялся.

private static Expression<Func<DomainObject, bool>> BuildExpression<TDest>(
    Expression<Func<DomainObject, TDest>> propertyexpression,
    string term
) where TDest : struct {
  //var property = Expression.Invoke(propertyexpression, propertyexpression.Parameters.ToArray());
  var property = Expression.Property(propertyexpression, ((MemberExpression)propertyexpression.Body).Member.Name);
  var parser = new ParsedSearchTerm<TDest>(term); // e.g. "<200" => { LowerBound = null, Operator = "<", UpperBound = 200 }

  Expression final = null;
  if (parser.HasLowerBound) {
    final = Expression.AndAlso(
      Expression.GreaterThanOrEqual(property, Expression.Constant(parser.LowerBound)),
      Expression.LessThanOrEqual(property, Expression.Constant(parser.UpperBound)));
  }
  else {
    switch (parser.Operator) {
      case "<":
        final = Expression.LessThanOrEqual(property, Expression.Constant(parser.UpperBound));
        break;
      case ">":
        final = Expression.GreaterThanOrEqual(property, Expression.Constant(parser.UpperBound));
        break;
      case "=":
        final = Expression.Equal(property, Expression.Constant(parser.UpperBound));
        break;
      case "!":
        final = Expression.Negate(Expression.Equal(property, Expression.Constant(parser.UpperBound)));
        break;
    }
  }

  return Expression.Lambda<Func<DomainObject, bool>>(final, propertyexpression.Parameters.ToArray());
}

1 Ответ

0 голосов
/ 20 декабря 2018

Чтобы ваш код, который вручную расширяет Invoke в лямбда-тело, вам нужно использовать тело лямбда-параметра (propertyexpression) в качестве значения property, которое вы хотите проверить:

var property = propertyexpression.Body;

(я бы переименовал propertyexpression в propertylambda - действительно, propertyexpression.Body - это выражение свойства).

Вы можете использовать исходную лямбду с EF, если заменить Invoke на расширениеэто делает расширение на месте лямбда-тела propertylambda с аргументами, заменяемыми лямбда-параметрами.Я называю это Apply.

Учитывая некоторые Expression методы расширения:

public static class ExpressionExt {
    /// <summary>
    /// Replaces a sub-Expression with another Expression inside an Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Zip(args, (p, a) => (p, a)))
            b = b.Replace(pa.p, pa.a);

        return b.PropagateNull();
    }
}

и некоторые ExpressionVisitor классы для внесения изменений:

/// <summary>
/// Standard ExpressionVisitor to replace an Expression with another in an Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

/// <summary>
/// ExpressionVisitor to replace a null.member Expression with a null
/// </summary>
public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
    public override Expression Visit(Expression node) {
        if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
            return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
        else
            return base.Visit(node);
    }
}

Вы можете взять любой экземпляр Expression.Invoke(lambda,args) и заменить его на Apply(lambda, args), и он расширит лямбда-тело в строке, так что EF примет его.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...