Generic Expression tree with 'OR' clause for each supplied property
c#, expression-trees, generics, lambda
Solution
Here we go; you were pretty close - as I noted in comments, the key piece here is to use `ExpressionVisitor` to re-write the trees in terms of the single parameter you want to keep:
using System;
using System.Linq;
using System.Linq.Expressions;
static class Program
{
static void Main()
{
var data = new[] { new Foo { A = "x1", B = "y1", C = "y1" }, new Foo { A = "y2", B = "y2", C = "y2" },
new Foo { A = "y3", B = "y3", C = "x3" } }.AsQueryable();
var result = data.Search("x", x => x.A, x => x.B, x => x.C);
foreach (var row in result)
{
Console.WriteLine("{0}, {1}, {2}", row.A, row.B, row.C);
}
}
class Foo
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
public class SwapVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public SwapVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
public static Expression Swap(Expression body, Expression from, Expression to)
{
return new SwapVisitor(from, to).Visit(body);
}
}
public static IQueryable<TSource> Search<TSource>(this IQueryable<TSource> source, string searchTerm, params Expression<Func<TSource, string>>[] stringProperties)
{
if (String.IsNullOrEmpty(searchTerm))
{
return source;
}
if (stringProperties.Length == 0) return source.Where(x => false);
// The lamda I would like to reproduce:
// source.Where(x => x.[property1].Contains(searchTerm)
// || x.[property2].Contains(searchTerm)
// || x.[property3].Contains(searchTerm)...)
//Create expression to represent x.[property1].Contains(searchTerm)
var searchTermExpression = Expression.Constant(searchTerm);
var param = stringProperties[0].Parameters.Single();
Expression orExpression = null;
//Build a contains expression for each property
foreach (var stringProperty in stringProperties)
{
// re-write the property using the param we want to keep
var body = SwapVisitor.Swap(stringProperty.Body, stringProperty.Parameters.Single(), param);
var checkContainsExpression = Expression.Call(
body, typeof(string).GetMethod("Contains"), searchTermExpression);
if (orExpression == null)
{
orExpression = checkContainsExpression;
}
else
{ // compose
orExpression = Expression.OrElse(orExpression, checkContainsExpression);
}
}
var lambda = Expression.Lambda<Func<TSource, bool>>(orExpression, param);
return source.Where(lambda);
}
}
Problem
I have created a generic search extension method for IQueryable that enables you to search for a single property to see if a search term is contained within it. http://jnye.co/Posts/6/c%23-generic-search-extension-method-for-iqueryable I now want to enable the user to select multiple properties to search within each, matching if any property contains the text. The code: The user enters the following code to perform this search: ``` string searchTerm = "Essex"; context.Clubs.Search(searchTerm, club => club.Name, club => club.County) //Note: If possible I would rather something closer to the following syntax... context.Clubs.Search(club => new[]{ club.Name, club.County}, searchTerm); // ... or, even better, something similar to this... context.Clubs.Search(club => new { club.Name, club.County}, searchTerm); ``` This will return any golf club with 'Essex' in the Name or as the County. ``` public static IQueryable<TSource> Search<TSource>(this IQueryable<TSource> source, string searchTerm, params Expression<Func<TSource, string>>[] stringProperties) { if (String.IsNullOrEmpty(searchTerm)) { return source; } // The lamda I would like to reproduce: // source.Where(x => x.[property1].Contains(searchTerm) // || x.[property2].Contains(searchTerm) // || x.[property3].Contains(searchTerm)...) //Create expression to represent x.[property1].Contains(searchTerm) var searchTermExpression = Expression.Constant(searchTerm); //Build parameters var parameters = stringProperties.SelectMany(prop => prop.Parameters); Expression orExpression = null; //Build a contains expression for each property foreach (var stringProperty in stringProperties) { var checkContainsExpression = Expression.Call(stringProperty.Body, typeof(string).GetMethod("Contains"), searchTermExpression); if (orExpression == null) { orExpression = checkContainsExpression; } //Build or expression for each property orExpression = Expression.OrElse(orExpression, checkContainsExpression); } var methodCallExpression = Expression.Call(typeof(Queryable), "Where", new Type[] { source.ElementType }, source.Expression, Expression.Lambda<Func<TSource, bool>>(orExpression, parameters)); return source.Provider.CreateQuery<TSource>(methodCallExpression); } ``` The error If I change the number of parameters supplied to 1: ``` Expression.Lambda<Func<TSource, bool>>(orExpression, parameters.First())); ``` I get a new error: UPDATE I have written a post on the work discussed in this question. Check it out on GitHub too.