views:

257

answers:

5

I have a user control which takes a Func which it then gives to the Linq "Where" extension method of a IQueryable. The idea is that from the calling code, I can pass in the desired search function.

I'd like to build this search function dynamically as such:

Func<Order, bool> func == a => true;
if (txtName.Text.Length > 0) {
  //add it to the function
  func = a => func(a) && a.Name.StartsWith(txtName.Text);
}
if (txtType.Text.Length > 0) {
  //add it to the function
  func = a => func(a) && a.Type == txtType.Text;
}
..... etc .....

The problem with this approach is that since I'm reusing the name "func" it creates a recursive function.

Is there an easy way to build out the expression tree like this to make a dynamic where clause (in the absence of having the IQueryable up front and repeatedly calling "Where")?

+1  A: 

Just save the current lambda in a temporary variable to prevent recursion.

var tempFunc = func;
func = a => tempFunc(a) && ...
Dario
this works, wish I'd seen it 5 mins ago. lol. for a cleaner way, I used an extension method...
TheSoftwareJedi
That won't work for expression-based lambdas for IQueryable
Marc Gravell
A: 

If you're going to be using this in LinqToSQL or any other dynamic expression tree parser, you're going to want to use PredicateBuilder!!!

Otherwise:

This extension method will prevent the recursion:

    public static Func<T, bool> And<T>(this Func<T, bool> f1, Func<T, bool> f2)
    {
        return a => f1(a) && f2(a);
    }

    public static Func<T, bool> Or<T>(this Func<T, bool> f1, Func<T, bool> f2)
    {
        return a => f1(a) || f2(a);
    }

Use it like so:

Func<Order, bool> func == a => true;
if (txtName.Text.Length > 0) {
  //add it to the function
  func.And(a => a.Name.StartsWith(txtName.Text));
}
if (txtType.Text.Length > 0) {
  //add it to the function
  func.And(a => a.Type == txtType.Text);
}
TheSoftwareJedi
A: 

I am right in the middle of doing exactly this... I am using Expressions because Func is compiled code where as Expression<Func<YourObect, boo>> can be converted C# or TSql or what ever... I just have seen several people recommend using expression instead of just func.

On you search page you would implement the code like this:

SearchCritera<Customer> crit = new SearchCriteria<Customer>();

if (txtName.Text.Length > 0) {
  //add it to the function
  crit.Add(a.Name.StartsWith(txtName.Text));
}

if (txtType.Text.Length > 0) {
  //add it to the function
  crit.Add(a.Type == txtType.Text));
}

The SearchCriteria object look something like this...

public class SearchCritera<TEntity>
    {
     private List<Expression<Func<TEntity, bool>>> _Criteria = new List<Expression<Func<TEntity, bool>>>();

     public void Add(Expression<Func<TEntity, bool>> predicate)
     {
      _Criteria.Add(predicate);
     }

     // This is where your list of Expression get built into a single Expression 
     // to use in your Where clause
     public Expression<Func<TEntity, bool>> BuildWhereExpression()
     {
      Expression<Func<TEntity, bool>> result = default(Expression<Func<TEntity, bool>>);
      ParameterExpression parameter = Expression.Parameter(typeof(TEntity), "entity");
      Expression previous = _Criteria[0];

      for (int i = 1; i < _Criteria.Count; i++)
      {
       previous = Expression.And(previous, _Criteria[i]);
      }

      result = Expression.Lambda<Func<TEntity, bool>>(previous, parameter);

      return result;
     }
    }

Then from your Where clause you could do this...

public List<Customer> FindAllCustomers(SearchCriteria criteria)
{
   return LinqToSqlDataContext.Customers.Where(SearchCriteria.BuildWhereExpression()).ToList();
}

This is the first time I have coded this out and you might need to make some changes for your purposes, I know it compliles but when I acutally go to do it I will unit test it, but it is the idea I have been tossing around in my head...

J.13.L
This gave a strange runtime error. However, I found the PredicateBuilder class (see my answer) and it works perfectly...
TheSoftwareJedi
+1  A: 

If you want to do an "and" combination, the preferred option is to use multiple "where" clauses:

IQueryable<Order> query = ...
if (!string.IsNullOrEmpty(txtName.Text.Length)) {
  //add it to the function
  query = query.Where(a => a.Name.StartsWith(txtName.Text));
}
if (!string.IsNullOrEmpty(txtType.Text.Length)) {
  //add it to the function
  query = query.Where(a => a.Type == txtType.Text);
}

You can do more complex things with expression building (AndAlso, Invoke, etc), but this is not necessary for an "and" combination.

If you really need to combine expressions, then the approach depends on the implementation. LINQ-to-SQL and LINQ-to-Objects support Expression.Invoke, allowing:

static Expression<Func<T, bool>> OrElse<T>(
    this Expression<Func<T, bool>> lhs,
    Expression<Func<T, bool>> rhs)
{
    var row = Expression.Parameter(typeof(T), "row");
    var body = Expression.OrElse(
        Expression.Invoke(lhs, row),
        Expression.Invoke(rhs, row));
    return Expression.Lambda<Func<T, bool>>(body, row);
}
static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> lhs,
    Expression<Func<T, bool>> rhs)
{
    var row = Expression.Parameter(typeof(T), "row");
    var body = Expression.AndAlso(
        Expression.Invoke(lhs, row),
        Expression.Invoke(rhs, row));
    return Expression.Lambda<Func<T, bool>>(body, row);
}

However, for Entity Framework you will usually need to rip the Expression apart and rebuild it, which is not easy. Hence why it is often preferable to use Queryable.Where (for "and") and Queryable.Concat (for "or").

Marc Gravell
A: 
Dictionary<Func<bool>, Expression<Func<Order, bool>>> filters =
  new Dictionary<Func<bool>, Expression<Func<Order, bool>>>();
// add a name filter
filters.Add(
  () => txtName.Text.Length > 0,
  a => a.Name.StartsWith(txtName.Text)
);
// add a type filter
filters.Add(
  () => txtType.Text.Length > 0,
  a => a.Type == txtType.Text
);

...

var query = dc.Orders.AsQueryable();

foreach( var filter in filters
  .Where(kvp => kvp.Key())
  .Select(kvp => kvp.Value) )
{
  var inScopeFilter = filter;
  query = query.Where(inScopeFilter);
}
David B