创建一个可重用的“元谓词”函数,接受lambda操作数参数

时间:2013-06-24 18:35:48

标签: linq entity-framework expression-trees

是否可以创建一个"元谓词"通过编写一个函数,该函数需要2(或4个必要的)lambda表示左侧和右侧属性(操作数)并使其生成谓词。类似下面的代码示例:

public Expression<Func<Something,bool>> StringEquals<Something>(Expression<Something> leftOperand, Expression<Something> leftOperandSelector){
    return (rightOperandSelector, leftOperandSelector) => (
        (rightOperandSelector == leftOperandSelector) 
        || (
            string.IsNullOrEmpty(rightOperandSelector) && 
            string.IsNullOrEmpty(leftOperandSelector)
        )
    );
}

OR:

public Expression<Func<Something,bool>> DatesActive<Something>(Expression<Something> startDateOperandSelector, Expression<Something> endDateOperandSelector){
    return (startDateOperandSelector, endDateOperandSelector) => (
    (startDatePropertySelector >= DateTime.Now) 
    && (endDatePropertySelector <= DateTime.Now)
    );
}

每一方的 SomeStringProperty startDatePropertySelector endDatePropertySelector 是由lambda定义的?我还没想出如何动态传递谓词表达式的操作数。

理想情况下,我希望能够像这样内联:

return new Expression<Func<Request,bool>>[]{
    r => (r.Id != request.Id) && (!r.Reviewed),
    StringEquals(r => r.VendorName, request=>request.VendorName),
    NotExpired(r => r.ContractStart, request=>request.ContractEnd),                        
    ...
};

* 有人对最佳方法有所了解吗?我的兴趣在于创建&#34; meta&#34; -expressions以方便使用,我在多个属性上重复使用相同的表达式。具体示例仅供参考/解释。非常愿意知道这是否愚蠢,如果有更好的方法,乐于学习。 *

如果需要,请在下方提供更多背景信息。

背景:在这个表达式的一个更简单的形式到位之后,我被迫重构来处理这样一个事实,即EntityFramework并没有像你期望的那样对待平等而不是解释C#如下:  (rightSide.SomeStringProperty == leftSide.SomeStringProperty) 像这样的两部分SQL表达式

(
    (rightSide.SomeStringProperty IS NULL AND leftSide.SomeStringProperty IS NULL)
    OR (rightSide.SomeStringProperty = leftSide.SomeStringProperty)
)

它更真实地将其转换为:

(rightSide.SomeStringProperty = leftSide.SomeStringProperty)

当然不会返回双方都为空的值。显然这已在EF6中得到纠正(更正:@Slauma指出这可以通过UseCSharpNullComparisonBehavior在EF5中使用。我使用的是EF4,无法升级此版本。)

我想避免更多重复的代码,如下所示:

            var where = new Expression<Func<Request,bool>>[]{
                    r => (r.Id != request.Id) && (!r.Reviewed) 
                    && (
                        (r.Address == request.Address) 
                        || (string.IsNullOrEmpty(r.Address) && string.IsNullOrEmpty(request.Address))
                    )
                    && (
                        (r.City == request.City) 
                        || (string.IsNullOrEmpty(r.City) && string.IsNullOrEmpty(request.City))
                    )
                    && (
                        (r.Province == request.Province) 
                        || (string.IsNullOrEmpty(r.Province) && string.IsNullOrEmpty(request.Province))
                    )
                    && (
                        (r.PostalCode == request.PostalCode) 
                        || (string.IsNullOrEmpty(r.PostalCode) && string.IsNullOrEmpty(request.PostalCode))
                    )
                    && (
                        (r.Website == request.Website) 
                        || (string.IsNullOrEmpty(r.Website) && string.IsNullOrEmpty(request.Website))
                    )
                };

2 个答案:

答案 0 :(得分:2)

您可以使用System.Linq.Expressions namespace手动构建表达式。对于您发布的两个示例,以下内容应该有效:

public static Expression<Func<T, bool>> StringEquals<T>(Expression<Func<T, string>> leftOperand, Expression<Func<T, string>> rightOperand)
{
   var p = leftOperand.Parameters[0];
   var leftOperandBody = leftOperand.Body;
   var rightOperandBody = ReplacementVisitor.Transform(rightOperand, rightOperand.Parameters[0], p);

   var isNullOrEmptyMethod = typeof(string).GetMethod("IsNullOrEmpty");
   var leftNullOrEmpty = Expression.Call(isNullOrEmptyMethod, leftOperandBody);
   var rightNullOrEmpty = Expression.Call(isNullOrEmptyMethod, rightOperandBody);
   var bothNullOrEmpty = Expression.AndAlso(leftNullOrEmpty, rightNullOrEmpty);
   var areEqual = Expression.Equal(leftOperandBody, rightOperandBody);
   var body = Expression.OrElse(bothNullOrEmpty, areEqual);

   return Expression.Lambda<Func<T, bool>>(body, p);
}

public static Expression<Func<T, bool>> DatesActive<T>(Expression<Func<T, DateTime>> startDate, Expression<Func<T, DateTime>> endDate)
{
   var p = startDate.Parameters[0];
   var startDateBody = startDate.Body;
   var endDateBody = ReplacementVisitor.Transform(endDate, endDate.Parameters[0], p);

   var nowProperty = typeof(DateTime).GetProperty("Now");
   var nowValue = Expression.Property(null, nowProperty);
   var startValid = Expression.GreaterThanOrEqual(startDateBody, nowValue);
   var endValid = Expression.LessThanOrEqual(endDateBody, nowValue);
   var body = Expression.AndAlso(startValid, endValid);

   return Expression.Lambda<Func<T, bool>>(body, p);
}

internal sealed class ReplacementVisitor : ExpressionVisitor
{
   private IList<ParameterExpression> SourceParameters { get; set; }
   private Expression Find { get; set; }
   private Expression Replace { get; set; }

   public static Expression Transform(LambdaExpression source, Expression find, Expression replace)
   {
      var visitor = new ReplacementVisitor
      {
         SourceParameters = source.Parameters,
         Find = find,
         Replace = replace,
      };

      return visitor.Visit(source.Body);
   }

   private Expression ReplaceNode(Expression node)
   {
      return (node == Find) ? Replace : node;
   }

   protected override Expression VisitConstant(ConstantExpression node)
   {
      return ReplaceNode(node);
   }

   protected override Expression VisitBinary(BinaryExpression node)
   {
      var result = ReplaceNode(node);
      if (result == node) result = base.VisitBinary(node);
      return result;
   }

   protected override Expression VisitParameter(ParameterExpression node)
   {
      if (SourceParameters.Contains(node)) return ReplaceNode(node);
      return SourceParameters.FirstOrDefault(p => p.Name == node.Name) ?? node;
   }
}

答案 1 :(得分:0)

这可以通过动态编写表达式树来完成,正如Richard所展示的那样,尽管它不需要那么复杂。具体来说,访问者模式增加了不必要的开考虑这个简单的解决方案,你的第一个例子:

using E = System.Linq.Expressions.Expression;

/* ... */

private static readonly MethodInfo IsNullOrEmptyMethod = typeof(string).GetMethod("IsNullOrEmpty");

public static Expression<Func<bool>> StringEquals(Expression<Func<String>> leftAccessor, Expression<Func<String>> rightAccessor)
{
    var left = E.Parameter(typeof(string), "left");
    var right = E.Parameter(typeof(string), "left");

    // () => {
    //     string left = leftAccessor();
    //     string right = rightAccessor();
    //     
    //     return left == right ||
    //            string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right);
    // }

    return E.Lambda<Func<bool>>(
        E.Block(
            new[] { left, right },
            E.Assign(left, E.Invoke(leftAccessor)),
            E.Assign(right, E.Invoke(rightAccessor)),
            E.OrElse(
                E.Equal(left, right),
                E.AndAlso(
                    E.Call(IsNullOrEmptyMethod, left),
                    E.Call(IsNullOrEmptyMethod, right)))));
}

您可以应用类似的技术来设计第二个示例的解决方案。请注意,缺少任何通用参数:不需要公开包含属性的实际项。您可以使用此方法比较同一对象上的两个属性;两个不同对象上的相同属性;或任何任意值。

请注意,lambda编译器将负责内联() => r.Address之类的简单访问器。它可以很容易地做到这一点,因为访问器本身就是表达式。

编辑:再次阅读您的问题,我发现您正在使用Entity Framework。我不确定EF的查询提供程序是否足够复杂以内联访问者。如果不是,这可能不起作用,在这种情况下,可能需要像理查德在他的回答中那样做一些手动转换。我有兴趣听听这是否适用于您的情况。我将以任何一种方式留下这个答案,因为它可能对不使用EF的人有用。

另外,正如@svick在评论中指出的那样,EF几乎肯定不支持块表达式。您可能必须按如下方式构造lambda:

return E.Lambda<Func<bool>>(
    E.OrElse(
        E.Equal(E.Invoke(leftAccessor), E.Invoke(rightAccessor)),
        E.AndAlso(
            E.Call(IsNullOrEmptyMethod, E.Invoke(leftAccessor)),
            E.Call(IsNullOrEmptyMethod, E.Invoke(rightAccessor)))));