1

I am trying to come up with a utility method to build a Linq Query or Linq Predicate to add to an Linq to EF query to do search for all terms in a list of terms in a variable number of columns.

I am trying to use PredicateBuilder to build the where clause. With one search term and a fixed list of columns it is relatively easy. The pseudo code that I am trying to work up looks like this so far:

private static Predicate<Project> CreateDynamicSearch(IEnumerable<strings> searchableColumns, string[] searchTerms)
{
      var predicate = PredicateBuilder.True<Project>();
      foreach (var columnName in searchableColumns) 
      { 
        foreach (var term in searchTerms)
        {
          predicate = predicate.And(a => a.**columnName**.Contains(term));
        }
       predicate = predicate.Or(predicate);
      }
      return predicate;
}

The biggest issue I have is handling the expression for the columnName. Previous advice was to use an expression tree but I do not understand how that works into this scenario.

** Update ** I've taken the code as you have it after the update. It builds but when I actually make the call it errors on the Extension.Property(param,columnName); line, with the error Instance property 'Name' is not defined for type 'System.Func`2[Myclass,System.Boolean]' message. The columnName = "Name"

** Update 2 ** The way it's called:

var test = CreateDynamicSearch<Func<Project, bool>>(searchCols, searchTerms);
John S
  • 7,909
  • 21
  • 77
  • 145
  • Your code is very peculiar. `predicate = predicate.Or(predicate);` makes no sense. – NetMage Nov 20 '17 at 20:00
  • The idea is to find all records that have all search terms somewhere in the record. i.e. Firstname Lastname = John & Smith – John S Nov 20 '17 at 20:09
  • I'm afraid you will indeed have to create your own expression tree if this is what you want. But you could just as easily build your own SQL expression (providing you have no need to use the LINQ query with a non-SQL data store). However either way, there is a logic error in your sample, you'll need a temporary variable to build the "And" list for each column, which you can then combine with the existing predicate variable you have. – Dylan Nicholson Nov 20 '17 at 20:13
  • @JohnS I've updated answer, because conditions were mixed (because they are mixed in your post :)). You need to join `Contains` with OR (because match is when column contains any of provided term) and join those with AND, because match is when all provided columns satisfy criterias. – Evk Nov 20 '17 at 20:55

1 Answers1

2

You can build expression for predicate yourself, in this case it's relatively easy:

private static Expression<Func<T, bool>> CreateDynamicSearch<T>(IEnumerable<string> searchableColumns, string[] searchTerms) {
    // start with true, since we combine with AND
    // and true AND anything is the same as just anything
    var predicate = PredicateBuilder.True<T>();
    foreach (var columnName in searchableColumns) {                
        // start with false, because we combine with OR
        // and false OR anything is the same as just anything
        var columnFilter = PredicateBuilder.False<T>();
        foreach (var term in searchTerms) {
            // a =>
            var param = Expression.Parameter(typeof(T), "a");
            // a => a.ColumnName
            var prop = Expression.Property(param, columnName);
            // a => a.ColumnName.Contains(term)
            var call = Expression.Call(prop, "Contains", new Type[0], Expression.Constant(term));
            columnFilter = columnFilter.Or(Expression.Lambda<Func<T, bool>>(call, param));
        }
        predicate = predicate.And(columnFilter);
    }
    return predicate;
}

In response to comment

I was just curious if there was some way you could combine the expression created by Expression.Property(param, columnName) with the one the compiler generates for (string s) -> s.Contains(term)

You can do that with (for example) like this:

// a =>
var param = Expression.Parameter(typeof(T), "a");                    
// a => a.ColumnName
var prop = Expression.Property(param, columnName);                    
// s => s.Contains(term)
Expression<Func<string, bool>> contains = (string s) => s.Contains(term);
// extract body - s.Contains(term)
var containsBody = (MethodCallExpression)contains.Body;                    
// replace "s" parameter with our property - a.ColumnName.Contains(term)
// Update accepts new target as first parameter (old target in this case is 
// "s" parameter and new target is "a.ColumnName")
// and list of arguments (in this case it's "term" - we don't need to update that).
// 
var call = containsBody.Update(prop, containsBody.Arguments);
columnFilter = columnFilter.Or(Expression.Lambda<Func<T, bool>>(call, param));
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Added the error I now get. Any thoughts on what that is about? – John S Nov 20 '17 at 22:11
  • @JohnS can you include how you call that method? – Evk Nov 20 '17 at 22:13
  • 1
    @JohnS from the error it seems you are calling it like `CreateDynamicSearch>()` but you need to just `CreateDynamicSearch`. – Evk Nov 20 '17 at 22:16
  • I just added the call. :) – John S Nov 20 '17 at 22:19
  • 1
    @JohnS And I wrote above what's wrong in that call :) – Evk Nov 20 '17 at 22:19
  • That worked great. Thanks! Now I just have to figure out how to make it work with the fields in the included tables. – John S Nov 20 '17 at 22:23
  • 1
    You basically call `Expression.Property` multiple times. `Expression.Property(Expression.Property(param, navigationPropertyName), columnOfIncludedEntity))`. If nesting level is unknown - you use a loop. Well I hope you'll figure that out yourself :) – Evk Nov 20 '17 at 22:25
  • @Evk - you obviously know your Expression tree building code pretty well - I tried doing the same sort of thing but trying to make use of the compiler's ability to generate an Expression out of (string s) -> s.Contains(term), given that much is known at compile time. Didn't have much luck with it though, and even looking at the .net source I can't see how you'd do it. – Dylan Nicholson Nov 21 '17 at 09:38
  • @DylanNicholson maybe you can ask question about that, because it's not very clear from your comment what is the issue exactly. – Evk Nov 21 '17 at 09:43
  • I was just curious if there was some way you could combine the expression created by `Expression.Property(param, columnName)` with the one the compiler generates for `(string s) -> s.Contains(term)`. It's probably possible but the result is necessarily more complex/less efficient, nor sure how it would translate to SQL. – Dylan Nicholson Nov 21 '17 at 09:45
  • @DylanNicholson I've updated answer with one example of how this can be done – Evk Nov 21 '17 at 09:57
  • `Expression.Invoke(((Expression>)((String s) => s.Contains(term))).Body, prop)` does it, but at no gain in legibility or maintainability I can see! Your answer is definitely better, would've taken me a while to figure that out. – Dylan Nicholson Nov 21 '17 at 09:58
  • @DylanNicholson and if you have more complex expression so that solution above (with `Update`) doesn't help - then you can use expression visitors to replace stuff inside expression with what you need. – Evk Nov 21 '17 at 10:07