3

This is the tutorial I'm following to learn Expression Tree.

I've more than 35 columns to display, but the user can chose to display 10 columns at once. So one the user type something in the search box, I want to search only the columns that are visible to the user.

SELECT FirstName, LastName, Address, ..., State
FROM Students
WHERE Id == @Id col1 AND (
      FirstName LIKE '%@searchText%' OR 
      LastName LIKE '%@searchText%' OR 
      Address LIKE '%@searchText%' OR 
      ...
      State LIKE '%@searchText%')

Back to Linq, this is how I'm trying to accomplish it:

var result = db.Students
    .Where(GetPredicate(id, listOfColumns))
    .ToList();

This the private method:

private Expression<Func<Student, bool>> GetPredicate(int id, List<string> listOfColumns)
{
   ParameterExpression pe = Expression.Parameter(typeof(Student), "s");

   Expression left0 = Expression.Property(pe, "Id");
   Expression right0 = Expression.Constant(id);
   Expression e0 = Expression.Equal(left0, right0);

   //Here ... omitted code because it's not working...
   //

   var expr = Expression.Lambda<Func<Student, bool>>(e0, new ParameterExpression[] { pe });
        return expr;
}

As it is above, it's working just fine. However, the reason I even wrote this method was to be able to filter only by the user-selected columns.

I want to be able to compose based on the column that are visible in the UI.

if(!string.IsNullOrEmpty(searchText))
{
   foreach (string columnName in columnList)
   {
       Expression col = Expression.Property(pe, columnName);
       Expression left = Expression.Call(pe, typeof(string).GetMethod("Contains"));
       Expression right = Expression.Constant(searchText);
       Expression e = Expression.IsTrue(left, right);
   }
}

I'm completely lost. I know that I need to access the Contains method of the string class then I don't know what next. The Idea is to get something like this:

Where(d => d.Id == id && (d.FirstName.Contains(searchText) 
        || d.LastName.Contains(searchText) 
        || ...
        || d.State.Contains(searchText)))

Thanks for helping

Richard77
  • 20,343
  • 46
  • 150
  • 252
  • 1
    Although the duplicate asks for the same thing, the answer provides information for only part of the problem, on which OP was pretty close anyway. The other part, where multiple expressions are combined into a single chain of "OR"s, is left out from all answers to the duplicate question, including the accepted one. Voting to re-open this question to provide the explanation for the part missing in the duplicate. – Sergey Kalinichenko May 26 '17 at 20:18
  • @dasblinkenlight Both parts of the problem (and now the third one about nested property accessor in comments under your answer) are duplicates and have been answered. I've just picked the one that matches the post title. In general they should have been asked (and hence closed as duplicates:) separately. – Ivan Stoev May 27 '17 at 13:07

2 Answers2

6

You are pretty close, except constructing the call of Contains does not have a right side:

Expression col = Expression.Property(pe, columnName);
Expression contains = Expression.Call(
    pe
,   typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string)}) // Make a static field out of this
,   Expression.Constant(searchText) // Prepare a shared object before the loop
);

Once you have your call expressions, combine them with OrElse to produce the body of your lambda. You can do it with loops, or you can use LINQ:

private static readonly MethodInfo Contains = typeof(string)
    .GetMethod(nameof(string.Contains), new Type[] { typeof(string)});

public static Expression<Func<Student,bool>> SearchPredicate(IEnumerable<string> properties, string searchText) {
    var param = Expression.Parameter(typeof(Student));
    var search = Expression.Constant(searchText);
    var components = properties
        .Select(propName => Expression.Call(Expression.Property(param, propName), Contains, search))
        .Cast<Expression>()
        .ToList();
    // This is the part that you were missing
    var body = components
        .Skip(1)
        .Aggregate(components[0], Expression.OrElse);
    return Expression.Lambda<Func<Student, bool>>(body, param);
}
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • It's working like charm. I was even able to combine the `s.Id == id` condition with the one you provided, using `Expression.AndAlso()`. What if the property I'm interested in is also a property of another property? Let's through the navigation property: `p => p.Category.CategoryId == categoryId`. – Richard77 May 27 '17 at 00:03
  • @Richard77 When you need a property of a property, construct `Expression.Property(Expression.Property(param, "Category"), "CategoryId")`. You can do it in a loop, too - split `"Category.CategoryId"` on the dot `'.'`, then keep calling `Expression.Property` on the parts, using the previous expression as the first argument. – Sergey Kalinichenko May 27 '17 at 00:20
  • Contains is Now Ambiguous (.net 6) The following works for string.contains(string) : private static readonly MethodInfo Contains = typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string)}); – Tod Nov 17 '21 at 17:55
  • 1
    @Tod Thank you, this is now fixed. – Sergey Kalinichenko Nov 17 '21 at 18:00
0

I like the PredicateBuilder class for stuff like this scenario:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;

public static class PredicateBuilder
{
  public static Expression<Func<T, bool>> True<T> ()  { return f => true;  }
  public static Expression<Func<T, bool>> False<T> () { return f => false; }

  public static Expression<Func<T, bool>> Or<T> (this Expression<Func<T, bool>> expr1,
                                                      Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters);
  }

  public static Expression<Func<T, bool>> And<T> (this Expression<Func<T, bool>> expr1,
                                                       Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters);
  }
}

Code using this would look like:

var predicate = PredicateBuilder.True<Student>().And(i=>i.Id==id);
if(!string.IsNullOrEmpty(searchText))
{
    if (firstNameColumnVisible) {
       predicate = predicate.And(i=>i.FirstName.Contains(searchText));
    }
    if (lastNameColumnVisible) {
       predicate = predicate.And(i=>i.LastName.Contains(searchText));
    }
    // more columns here.
}

At the end, use the PredicateBuilder instance as arguments to your Where operator in the Linq query.

Xavier J
  • 4,326
  • 1
  • 14
  • 25