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

3
我有以下方法来构建一些自定义的EF查询,以支持一个非常接近工作的文本过滤器,但是我在组装表达式的左侧遇到了问题。当我使用"Expression.Invoke"(方法体的第一行)时,我会得到一个异常,其中LINQ表达式节点类型'Invoke'在LINQ to Entities中不受支持。这对我来说是有意义的(我概念上理解了在LINQ => SQL翻译中发生了什么)。所以我想左边的表达式必须需要像右边那样需要更多的东西(即使用Expression.Constant),在所有的“预处理”完成后,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());
}

上面的代码不应该会给你那个错误信息。你注释掉的那一行是不是有责任呢? - DavidG
1
propertyexpression 是一个 Func<>:类型 Func 没有名为 Member.Name(=PropertyName)的属性。只需输入 var property = propertyexpression.Body; - NetMage
1
注意:我已经处理了将 Expression.Invoke 转换为 EF 支持的内容,用于我的左外连接 IQueryable 实现,通过使用扩展方法 Apply 来实现,该方法通过使用 ExpressionVisitor 将参数替换为参数来扩展 lambda 体,如 此答案 所示(您可能不需要在 Apply 上使用 PropagateNull)。您只需在使用 Expression.Invoke 的位置替换为 Apply 即可。 - NetMage
@NetMage,你的第一条评论非常到位。您能将其添加为答案,我会接受吗?简直不敢相信这么简单 :-P - BLSully
我也添加了“应用”代码... - NetMage
1个回答

3
为了让您的代码手动将Invoke扩展为lambda体,您需要使用lambda参数(propertyexpression)的主体作为您想要测试的property值:
var property = propertyexpression.Body;

我建议将 propertyexpression 重命名为 propertylambda,实际上,propertyexpression.Body 就是属性表达式。
如果你使用 propertylambda 并用参数替换 lambda 参数,可以在 EF 中使用原始 lambda,只需用一个扩展方法 Apply 来代替 Invoke,该方法会对 propertylambda 的 lambda 主体进行原地扩展。
给定一些 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中内联扩展lambda主体,以便EF接受它。


谢谢您的解释。我需要再考虑一下。我对Expression命名空间还很陌生。而且变量的重命名帮助我意识到为什么应该那样做。现在更有意义了。 - BLSully
我会说,结合可能会很复杂,因此创建了像LINQKit这样的库,以使它更容易、更自动化,并且ExpressionVisitor可以非常强大,只需几个自定义访问器就可以实现困难的事情。 - NetMage

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接