1

是否可以通过编写一个函数来创建一个“元谓词”,该函数需要 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)
        )
    );
}

或者:

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),                        
    ...
};

*有人对解决此问题的最佳方法有想法吗?我的兴趣是创建易于使用的“元”表达式,我在多个属性上重复使用相同的表达式。具体示例仅供参考/解释。非常愿意知道这是否愚蠢以及是否有更好的方法,很高兴学习。*

如果需要,下面有更多背景。

背景:在此表达式的更简单形式到位后,我被迫重构以处理 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))
                    )
                };
4

2 回答 2

2

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

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;
   }
}
于 2013-06-25T18:48:32.053 回答
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. 它可以很容易地做到这一点,因为访问器本身就是表达式。

编辑:再次阅读您的问题,我看到您正在使用实体框架。我不确定 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)))));
于 2013-06-26T14:40:15.360 回答