Extend IQueryable Where() as OR instead of AND relationship

后端 未结 4 1852
有刺的猬
有刺的猬 2020-12-16 23:15

I am using my own extension methods of IQueryable<> to create chainable queries such as FindAll().FindInZip(12345).NameStartsWith(\"XYZ\").OrderByHowIWantIt() etc. which

相关标签:
4条回答
  • 2020-12-16 23:53

    I'm assuming the different parts of the query are only known at runtime, i.e. you can't just use || in a where...

    One lazy option is Concat - but this tends to lead to poor TSQL etc; however, I tend to be inclined to write custom Expressions instead. The approach to take depends on what the provider is, as LINQ-to-SQL supports different options to EF (for example) - which has a genuine impact here (since you can't use sub-expressions with EF). Can you tell us which?


    Here's some code that should work with LINQ-to-SQL; if you build an array (or list, and call .ToArray()) of expressions, it should work fine; example is LINQ-to-Objects, but should still work:

        static void Main()
        {
            var data = (new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }).AsQueryable();
    
            var predicates = new List<Expression<Func<int, bool>>>();
            predicates.Add(i => i % 3 == 0);
            predicates.Add(i => i >= 8);           
    
            foreach (var item in data.WhereAny(predicates.ToArray()))
            {
                Console.WriteLine(item);
            }
        }
    
        public static IQueryable<T> WhereAny<T>(
            this IQueryable<T> source,
            params Expression<Func<T,bool>>[] predicates)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (predicates == null) throw new ArgumentNullException("predicates");
            if (predicates.Length == 0) return source.Where(x => false); // no matches!
            if (predicates.Length == 1) return source.Where(predicates[0]); // simple
    
            var param = Expression.Parameter(typeof(T), "x");
            Expression body = Expression.Invoke(predicates[0], param);
            for (int i = 1; i < predicates.Length; i++)
            {
                body = Expression.OrElse(body, Expression.Invoke(predicates[i], param));
            }
            var lambda = Expression.Lambda<Func<T, bool>>(body, param);
            return source.Where(lambda);
        }
    
    0 讨论(0)
  • 2020-12-17 00:07

    In an ideal world I personally think || and && operators would be the most simple and readable. However it won't compile.

    operator ' ||' cannot be applied to operands of type 'Expression<Func<YourClass,bool>>' and 'Expression<Func<YourClass,bool>>'

    Therefore I use an extension method for this. In your example it would look like this: .Where(FindInZip(12345).Or(NameStartsWith("XYZ")).And(PostedOnOrAfter(DateTime.Now)).

    Instead of:

    .Where(FindInZip(12345) || NameStartsWith("XYZ") && (PostedOnOrAfter(DateTime.Now)).

    Expression example:

    private Expression<Func<Post,bool>> PostedOnOrAfter(DateTime cutoffDate)
    {
          return post => post.PostedOn >= cutoffDate;
    };
    

    Extension method:

    public  static  class PredicateExtensions
    {
         ///  <summary>
         /// Begin an expression chain
         ///  </summary>
         ///  <typeparam id="T""></typeparam>
         ///  <param id="value"">Default return value if the chanin is ended early</param>
         ///  <returns>A lambda expression stub</returns>
         public  static Expression<Func<T,  bool>> Begin<T>(bool value =  false)
        {
             if (value)
                 return parameter =>  true;  //value cannot be used in place of true/false
    
             return parameter =>  false;
        }
    
         public  static Expression<Func<T,  bool>> And<T>(this Expression<Func<T,  bool>> left,
            Expression<Func<T,  bool>> right)
        {
             return CombineLambdas(left, right, ExpressionType.AndAlso);
        }
    
         public  static Expression<Func<T,  bool>> Or<T>(this Expression<Func<T,  bool>> left, Expression<Func<T,  bool>> right)
        {
             return CombineLambdas(left, right, ExpressionType.OrElse);
        }
    
         #region private
    
         private  static Expression<Func<T,  bool>> CombineLambdas<T>(this Expression<Func<T,  bool>> left,
            Expression<Func<T,  bool>> right, ExpressionType expressionType)
        {
             //Remove expressions created with Begin<T>()
             if (IsExpressionBodyConstant(left))
                 return (right);
    
            ParameterExpression p = left.Parameters[0];
    
            SubstituteParameterVisitor visitor =  new SubstituteParameterVisitor();
            visitor.Sub[right.Parameters[0]] = p;
    
            Expression body = Expression.MakeBinary(expressionType, left.Body, visitor.Visit(right.Body));
             return Expression.Lambda<Func<T,  bool>>(body, p);
        }
    
         private  static  bool IsExpressionBodyConstant<T>(Expression<Func<T,  bool>> left)
        {
             return left.Body.NodeType == ExpressionType.Constant;
        }
    
         internal  class SubstituteParameterVisitor : ExpressionVisitor
        {
             public Dictionary<Expression, Expression> Sub =  new Dictionary<Expression, Expression>();
    
             protected  override Expression VisitParameter(ParameterExpression node)
            {
                Expression newValue;
                 if (Sub.TryGetValue(node,  out newValue))
                {
                     return newValue;
                }
                 return node;
            }
        }
    
         #endregion
    } 
    

    A really good article about LINQ Queries by Extending Expressions. Also the source of the extension method that I use.

    https://www.red-gate.com/simple-talk/dotnet/net-framework/giving-clarity-to-linq-queries-by-extending-expressions/

    0 讨论(0)
  • 2020-12-17 00:09
        List<string> fruits =
            new List<string> { "apple", "passionfruit", "banana", "mango",
                   "orange", "blueberry", "grape", "strawberry" };
    
        var query = fruits.AsQueryable();
    
        // Get all strings whose length is less than 6.
        query = query.Where(fruit => fruit.Length < 6);
    
        // Hope to get others where length is more than 8.  But you can't, they're gone.
        query = query.Where(fruit => 1 == 1 || fruit.Length > 8);
    
        foreach (string fruit in query)
            Console.WriteLine(fruit);
    
    0 讨论(0)
  • 2020-12-17 00:11

    Use PredicateBuilder<T>. It's probably what you want.

    0 讨论(0)
提交回复
热议问题