为 Entity Framework 构建自定义表达式 (LINQ)

Building Custom Expressions for Entity Framework (LINQ)

我有以下方法来构建一些自定义 EF 查询以支持非常接近工作的文本过滤器,但我在组装表达式的左侧遇到问题。当我使用 "Expression.Invoke" (方法体的第一行)时,我得到一个异常 The LINQ expression node type 'Invoke' is not supported in LINQ to Entities. 这对我来说很有意义(我从概念上理解 LINQ => SQL 翻译中发生了什么) .所以我认为表达式的左侧必须需要更像右侧的东西(即使用 Expression.Constant),其中所有 'preprocessing' 都已完成,因此 LINQ to Entities 知道如何构造表达式的左侧.

但是当我使用第二行(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());
}

要使您的代码手动将 Invoke 扩展到 lambda 主体中,您需要使用 lambda 参数 (propertyexpression) 的主体作为您想要的 property 值测试:

var property = propertyexpression.Body;

(我会将 propertyexpression 重命名为 propertylambda - 实际上,propertyexpression.Body 是 属性 表达式)。

如果您将 Invoke 替换为一个扩展名,该扩展名对 propertylambda 的 lambda 主体进行就地扩展,并且将参数替换为 lambda,则您可以将原始 lambda 与 EF 一起使用参数。我称之为 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),它会在线扩展 lambda 主体,以便 EF 接受它。