views:

79

answers:

2

(An earlier question, http://stackoverflow.com/questions/3782799/recursively-compose-linq-predicates-into-a-single-predicate, is similar to this but I actually asked the wrong question... the solution there satisfied the question as posed, but isn't actually what I need. They are different, though. Honest.)

Given the following search text:

"keyword1 keyword2 ... keywordN"

I want to end up with the following SQL:

SELECT [columns] FROM Customer 
  WHERE (
        Customer.Forenames LIKE '%keyword1%' 
        OR 
        Customer.Forenames LIKE '%keyword2%'
        OR
        ...
        OR
        Customer.Forenames LIKE '%keywordN%'
    ) AND (
        Customer.Surname LIKE '%keyword1%' 
        OR 
        Customer.Surname LIKE '%keyword2%'
        OR
        ....
        OR
        Customer.Surname LIKE '%keywordN%'
    )

Effectively, we're splitting the search text on spaces, trimming each token, constructing a multi-part OR clause based on each , and then AND'ing the clauses together.

I'm doing this in Linq-to-SQL, and I have no idea how to dynamically compose a predicate based on an arbitrarily-long list of subpredicates. For a known number of clauses, it's easy to compose the predicates manually:

dataContext.Customers.Where(
    ( 
      Customer.Forenames.Contains("keyword1") 
      ||
      Customer.Forenames.Contains("keyword2")
    ) && (
      Customer.Surname.Contains("keyword1") 
      ||
      Customer.Surname.Contains("keyword2")
    )
);

In short, I need a technique that, given two predicates, will return a single predicate composing the two source predicates with a supplied operator, but restricted to the operators explicitly supported by Linq-to-SQL. Any ideas?

A: 

Normally you would chain invocations of .Where(...). E.g.:

var a = dataContext.Customers;
if (kwd1 != null)
    a = a.Where(t => t.Customer.Forenames.Contains(kwd1));
if (kwd2 != null)
    a = a.Where(t => t.Customer.Forenames.Contains(kwd2));
// ...
return a;

LINQ-to-SQL would weld it all back together into a single WHERE clause.

This doesn't work with OR, however. You could use unions and intersections, but I'm not sure whether LINQ-to-SQL (or SQL Server) is clever enough to fold it back to a single WHERE clause. OTOH, it won't matter if performance doesn't suffer. Anyway, it would look something like this:

<The type of dataContext.Customers> ff = null, ss = null;

foreach (k in keywords) {
    if (keywords != null) {
        var f = dataContext.Customers.Where(t => t.Customer.Forenames.Contains(k));
        ff = ff == null ? f : ff.Union(f);

        var s = dataContext.Customers.Where(t => t.Customer.Surname.Contains(k));
        ss = ss == null ? s : ss.Union(s);
    }
}
return ff.Intersect(ss);
Marcelo Cantos
@Marcelo: How do you do OR using that?
Matti Virkkunen
Ah, silly me. I didn't pay attention to the `||`s.
Marcelo Cantos
I've amended my answer. `PredicateBuilder` looks like a better solution, but I'll leave mine here just in case.
Marcelo Cantos
+5  A: 

You can use the PredicateBuilder class

IQueryable<Customer> SearchCustomers (params string[] keywords)
{
  var predicate = PredicateBuilder.False<Customer>();

  foreach (string keyword in keywords)
  {
    // Note that you *must* declare a variable inside the loop
    // otherwise all your lambdas end up referencing whatever
    // the value of "keyword" is when they're finally executed.
    string temp = keyword;
    predicate = predicate.Or (p => p.Forenames.Contains (temp));
  }
  return dataContext.Customers.Where (predicate);
}

(that's actually the example from the PredicateBuilder page, I just adapted it to your case...)


EDIT:

Actually I misread your question, and my example above only covers a part of the solution... The following method should do what you want :

IQueryable<Customer> SearchCustomers (string[] forenameKeyWords, string[] surnameKeywords)
{
    var predicate = PredicateBuilder.True<Customer>();

    var forenamePredicate = PredicateBuilder.False<Customer>();
    foreach (string keyword in forenameKeyWords)
    {
      string temp = keyword;
      forenamePredicate = forenamePredicate.Or (p => p.Forenames.Contains (temp));
    }
    predicate = PredicateBuilder.And(forenamePredicate);

    var surnamePredicate = PredicateBuilder.False<Customer>();
    foreach (string keyword in surnameKeyWords)
    {
      string temp = keyword;
      surnamePredicate = surnamePredicate.Or (p => p.Surnames.Contains (temp));
    }
    predicate = PredicateBuilder.And(surnamePredicate);

    return dataContext.Customers.Where(predicate);
}

You can use it like that:

var query = SearchCustomers(
    new[] { "keyword1", "keyword2" },
    new[] { "keyword3", "keyword4" });

foreach (var Customer in query)
{
    ...
}
Thomas Levesque
Genius. Much obliged.
Dylan Beattie