I have a collection which is of type IQueryable, I need to sort this based on some dynamic sort fields. Sort fields are inside a list.
I write the following method to do this.
public List<T> Order<T>(IQueryable<T> source, List<string> propertyNames)
{
if(propertyNames != null && propertyNames.Count > 0)
{
var param = Expression.Parameter(typeof(T), string.Empty);
var property = Expression.PropertyOrField(param, propertyNames[0]);
var sort = Expression.Lambda(property, param);
MethodCallExpression orderByCall = Expression.Call(typeof(Queryable),"OrderBy",new[] { property.Type },Expression.Quote(sort));
if(propertyNames.Count > 1)
{
foreach(var item in propertyNames)
{
param = Expression.Parameter(typeof(T), string.Empty);
property = Expression.PropertyOrField(param, item);
sort = Expression.Lambda(property, param);
orderByCall = Expression.Call(
typeof(Queryable),
"ThenBy", new[] { typeof(T), property.Type },
orderByCall,
Expression.Quote(sort));
}
}
var results = (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(orderByCall);
if(results != null)
return results.ToList();
}
return null;
}
when I executed MethodCallExpression orderByCall = Expression.Call(typeof(Queryable),"OrderBy",new[] { property.Type },Expression.Quote(sort)); I got some exception
No generic method 'OrderBy' on type 'System.Linq.Queryable' is
compatible with the supplied type arguments and arguments. No type
arguments should be provided if the method is non-generic.
Sorry, I don't have a direct solution for your error.
Here is an alternative ("kept it simple") approach to dynamically order your data.
1) Add these extension methods somewhere in your project
public static IOrderedQueryable<TSource> OrderBy<TSource, TProperty>(this IQueryable<TSource> source
, Expression<Func<TSource, TProperty>> expression, bool descending)
{
return !descending ? source.OrderBy(expression) : source.OrderByDescending(expression);
}
public static IOrderedQueryable<TSource> ThenBy<TSource, TProperty>(this IOrderedQueryable<TSource> source
, Expression<Func<TSource, TProperty>> expression, bool descending)
{
return !descending ? source.ThenBy(expression) : source.ThenByDescending(expression);
}
2) Now you can just loop your list of property names and apply the OrderBy / ThenBy on your IQueryable.
Other idea: you could adapt your method so it accepts expressions instead of property name strings.
Related
I have a extension method which creates a lambda predicate expression, which I use to filter data when the value is not null.
It works fine for cases like
query.FilterBy(obj => obj.MyProp, value);
but it fails in cases like
query.FilterBy(obj => obj.MyObject.MyProp, value);
In this case the expression for MyProp property is accessed on obj, which of course doesn't have the property.
I'm not familiar with the expression tree and could not figure out how to modify the extension method to access properties of sub objects. Does anyone have an idea how I could do it?
The extension method:
public static IQueryable<TSource> FilterBy<TSource, TProp>(this IQueryable<TSource> source,
Expression<Func<TSource, TProp>> property,
TProp value)
{
if (value != null)
{
var parameter = Expression.Parameter(typeof(TSource));
var memberExpression = property.Body as MemberExpression ??
((UnaryExpression)property.Body).Operand as MemberExpression;
if (memberExpression is null)
{
throw new InvalidOperationException("Please provide a valid property expression.");
}
var propertyName = memberExpression.Member.Name;
var body = Expression.Equal(
Expression.Property(parameter, propertyName),
Expression.Constant(value));
var predicate = Expression.Lambda<Func<TSource, bool>>(body, parameter);
source = source.Where(predicate);
}
return source;
}
You could simply use epxression passed like parameter property of FilterBy() function for creating new filtering expression instead of trying to create expression from scratch. It will works with "sub-properties".
public static IQueryable<TSource> FilterBy<TSource, TProp>(this IQueryable<TSource> source,
Expression<Func<TSource, TProp>> property,
TProp value)
{
if (value != null)
{
var expression = Expression.Equal(property.Body, Expression.Constant(value));
var predicate = Expression.Lambda<Func<TSource, bool>>(expression, property.Parameters[0]);
source = source.Where(predicate);
return source;
}
return source;
}
I'm getting this exception when I run this code.
ParameterExpression of type System.Int64 cannot be used for delegate parameter of type System.Object
I know it's something to do with the Expression.Lambda<func<object,bool>> part of the code. Overall, I want to pass any type of ParameterExpression into this method and it will call the expression.
public static IQueryable<T> OrderData<T>(IQueryable<T> data)
{
try
{
Order order = Order.ASC;
var result = Enum.TryParse<Order>(_gridSettings.SortOrder, true, out order);
if (_gridSettings.IsSearch)
{
data = ExpressionSort(order, data, typeof(T).GetProperty(_gridSettings.SortColumn));
}
else
{
data = ExpressionSort(order, data, _defaultColumn);
}
}
catch (Exception ex)
{
log.WriteLog(MethodBase.GetCurrentMethod(), LogLevel.FATAL, ex);
}
return data;
}
private static IQueryable<T> ExpressionSort<T>(Order order, IQueryable<T> data, PropertyInfo property)
{
// Compose the expression tree that represents the parameter to the predicate.
ParameterExpression paramExpression = Expression.Parameter(property.PropertyType, property.Name);
IQueryable<T> queryableData = data.AsQueryable<T>();
switch (order)
{
case Order.ASC:
return ExecuteCall(paramExpression, paramExpression, queryableData, "OrderBy");
case Order.DESC:
return ExecuteCall(paramExpression, paramExpression, queryableData, "OrderByDescending");
}
return data;
}
private static IQueryable<T> ExecuteCall<T>(Expression expression, ParameterExpression paramExpression, IQueryable<T> queryableData, string linqMethod)
{
MethodCallExpression callExpression = Expression.Call(
typeof(Queryable),
linqMethod,
new Type[] { queryableData.ElementType },
queryableData.Expression,
Expression.Lambda<Func<object, bool>>(expression, new ParameterExpression[] { paramExpression }));
// Create an executable query from the expression tree.
return queryableData.Provider.CreateQuery<T>(callExpression);
}
EDIT:
I did see this answer to a similar question
Expression of type 'System.Int32' cannot be used for return type 'System.Object'
I do not know how to apply it to my code though
EDIT 2:
The main issue is that thisExpression.Lambda<Func<object, bool>>(conversion, new ParameterExpression[] { paramExpression })); line is giving me an exception. paramExpression contains an Int64 but its expectinng an object. I dont know how to dynamically tell the Func from the information I already have or if that is possible.
GOAL:
I am trying to do something like this data.OrderBy(x=>x.DynamicProperty);
This is what you asked for, I think... I've tested it and it seems to work.
// Caching of the reflection
private static readonly MethodInfo orderByMethod = GetOrderByMethod("OrderBy");
private static readonly MethodInfo orderByDescendingMethod = GetOrderByMethod("OrderByDescending");
private static IOrderedQueryable<TSource> ExpressionSort<TSource>(Order order, IQueryable<TSource> source, PropertyInfo property)
{
// Compose the expression tree that represents the parameter to
// the predicate.
// The expression you would use is source => source.Property,
// The parameter of the lambda, source
ParameterExpression sourceExpression = Expression.Parameter(typeof(TSource), "source");
// Accessing the expression
MemberExpression propertyExpression = Expression.Property(sourceExpression, property);
// The full lambda expression. We don't need the
// Expression.Lambda<>, but still the keySelector will be an
// Expression<Func<,>>, because Expression.Lambda does it
// authomatically. LambdaExpression is simply a superclass of
// all the Expression<Delegate>
LambdaExpression keySelector = Expression.Lambda(propertyExpression, sourceExpression);
// The OrderBy method we will be using, that we have cached
// in some static fields
MethodInfo method = order == Order.ASC ? orderByMethod : orderByDescendingMethod;
// Adapted from Queryable.OrderBy (retrieved from the reference
// source code), simply changed the way the OrderBy method is
// retrieved to "method"
return (IOrderedQueryable<TSource>)source.Provider.CreateQuery<TSource>(Expression.Call(null, method.MakeGenericMethod(new Type[]
{
typeof(TSource),
property.PropertyType
}), new Expression[]
{
source.Expression,
Expression.Quote(keySelector)
}));
}
private static MethodInfo GetOrderByMethod(string methodName)
{
// Here I'm taking the long and more correct way to find OrderBy/
// OrderByDescending: looking for a public static method with the
// right name, with two generic arguments and that has the
// parameters related to those two generic arguments in a certain
// way (they must be IQueryable<arg0> and Expression<Func<arg0,
// arg1>>
MethodInfo orderByMethod = (from x in typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public)
where x.Name == methodName
let generics = x.GetGenericArguments()
where generics.Length == 2
let parameters = x.GetParameters()
where parameters.Length == 2 &&
parameters[0].ParameterType == typeof(IQueryable<>).MakeGenericType(generics[0]) &&
parameters[1].ParameterType == typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(generics))
select x).Single();
return orderByMethod;
}
Please don't ever use AsQueryable<>(). It doesn't do what you think, and it is totally useless outside unit testing and very specific use cases.
You could use my OrderByString extension. https://www.nuget.org/packages/OrderByString/ It takes strings for sort parameters. The sort parameters strings can be comma-delimited lists of property names, such as "Prop1,Prop2" or it can include a sort order as in "Prop1 DESC, Prop2 ASC".
using OrderByExtensions;
public static IQueryable<T> OrderData<T>(IQueryable<T> data)
{
try
{
Order order = Order.ASC;
var result = Enum.TryParse<Order>(_gridSettings.SortOrder, true, out order);
var sortColumn = _gridSettings.IsSearch ? _gridSettings.SortColumn : _defaultColumn;
data = data.OrderBy(sortColumn + " " + _gridSettings.SortOrder.ToString());
}
catch (Exception ex)
{
log.WriteLog(MethodBase.GetCurrentMethod(), LogLevel.FATAL, ex);
}
return data;
}
OR
You could use the following GetExpressionForProperty method that returns the expected sort expression for OrderBy, OrderByDescending, ThenBy, or ThenByDescending.
private static IQueryable<T> ExpressionSort<T>(Order order, IQueryable<T> data, PropertyInfo property)
{
Expression<Func<T, object>> propertyExpression = GetExpressionForProperty<T>(property);
return order == Order.DESC ? data.OrderByDescending(propertyExpression) : data.OrderBy(propertyExpression);
}
static Expression<Func<TSource, object>> GetExpressionForProperty<TSource>(PropertyInfo propertyInfo)
{
var param = Expression.Parameter(typeof(TSource));
return Expression.Lambda<Func<TSource, object>>(
Expression.Convert(
Expression.Property(param, propertyInfo),
typeof(object)
)
, param);
}
Try using Expression.Convert. Here's a similar question that may give you some more guidance:
Expression of type 'System.Int32' cannot be used for return type 'System.Object'
I've looked into many generic linq filtering questions and their answers here in SO but none of them satisfy my needs so I thought I should create a question.
I've created many of what I call "filter provider" classes, one for each entity class in my model, to provide a simplistic search for my application. I didn't want to go into more advanced solutions like Lucene.Net because a basic filtering with matching score would suffice.
Inside each one of these provider classes there are multiple methods that will receive the filtering terms and query specific properties, returning a score for each match based on the relevance of the property. Most methods will filter multiple properties at once, but not all.
Here are two of these methods:
private IQueryable<Retailer> MatchHighRelevanceFields(string searchTerm, IQueryable<Retailer> retailers)
{
var results = retailers.Where(r =>
(r.CompanyName != null && r.CompanyName.ToUpper().Contains(searchTerm))
|| (r.TradingName != null && r.TradingName.ToUpper().Contains(searchTerm))
);
return results;
}
private IQueryable<Retailer> MatchMediumRelevanceFields(string searchTerm, IQueryable<Retailer> retailers)
{
var results = retailers.Where(r =>
(r.Address.Street != null && r.Address.Street.ToUpper().Contains(searchTerm))
|| (r.Address.Complement != null && r.Address.Complement.ToUpper().Contains(searchTerm))
);
return results;
}
These methods are replicated ad nauseum throughout each provider class and I hope I could replace them for a single method that would receive the properties to be included in the query.
Something like:
public static IQueryable<T> Match<T>(string searchTerm, IQueryable<T> data, Expression<Func<T, string>> filterProperties)
{
var results = **build the query for each property in filterProperties**
return results;
}
But I really can't figure it out. I tried using reflection but it only worked with Linq to Objects and I need a solution for Linq to Entities.
So to solve this problem we need a few puzzle pieces first. The first puzzle piece is a method that can take an expression that computes a value, and then another expression that computes a new value taking the same type the first returns, and creates a new expression that represents the result of passing the result of the first function as the parameter to the second. This allows us to Compose expressions:
public static Expression<Func<TFirstParam, TResult>>
Compose<TFirstParam, TIntermediate, TResult>(
this Expression<Func<TFirstParam, TIntermediate>> first,
Expression<Func<TIntermediate, TResult>> second)
{
var param = Expression.Parameter(typeof(TFirstParam), "param");
var newFirst = first.Body.Replace(first.Parameters[0], param);
var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}
This relies on the following tool to replace all instances of one expression with another:
public static Expression Replace(this Expression expression,
Expression searchEx, Expression replaceEx)
{
return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
}
We'll also need a tool to help us OR two predicate expressions together:
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 secondBody = expr2.Body.Replace(
expr2.Parameters[0], expr1.Parameters[0]);
return Expression.Lambda<Func<T, bool>>
(Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var secondBody = expr2.Body.Replace(
expr2.Parameters[0], expr1.Parameters[0]);
return Expression.Lambda<Func<T, bool>>
(Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
}
}
Now that we have this we can use Compose on each property selector to map it from the property results to whether or not that property value is non-null and contains the search term. We can then OR all of those predicates together to get a filter for your query:
public static IQueryable<T> Match<T>(
IQueryable<T> data,
string searchTerm,
IEnumerable<Expression<Func<T, string>>> filterProperties)
{
var predicates = filterProperties.Select(selector =>
selector.Compose(value =>
value != null && value.Contains(searchTerm)));
var filter = predicates.Aggregate(
PredicateBuilder.False<T>(),
(aggregate, next) => aggregate.Or(next));
return data.Where(filter);
}
You can do it with expression trees but it's not as simple as you might think.
public static IQueryable<T> Match<T>(this IQueryable<T> data, string searchTerm,
params Expression<Func<T, string>>[] filterProperties)
{
var parameter = Expression.Parameter(typeof (T), "source");
Expression body = null;
foreach (var prop in filterProperties)
{
// need to replace all the expressions with the one parameter (gist taken from Colin Meek blog see link on top of class)
//prop.body should be the member expression
var propValue =
prop.Body.ReplaceParameters(new Dictionary<ParameterExpression, ParameterExpression>()
{
{prop.Parameters[0], parameter}
});
// is null check
var isNull = Expression.NotEqual(propValue, Expression.Constant(null, typeof(string)));
// create a tuple so EF will parameterize the sql call
var searchTuple = Tuple.Create(searchTerm);
var matchTerm = Expression.Property(Expression.Constant(searchTuple), "Item1");
// call ToUpper
var toUpper = Expression.Call(propValue, "ToUpper", null);
// Call contains on the ToUpper
var contains = Expression.Call(toUpper, "Contains", null, matchTerm);
// And not null and contains
var and = Expression.AndAlso(isNull, contains);
// or in any additional properties
body = body == null ? and : Expression.OrElse(body, and);
}
if (body != null)
{
var where = Expression.Call(typeof (Queryable), "Where", new[] {typeof (T)}, data.Expression,
Expression.Lambda<Func<T, bool>>(body, parameter));
return data.Provider.CreateQuery<T>(where);
}
return data;
}
public static Expression ReplaceParameters(this Expression exp, IDictionary<ParameterExpression, ParameterExpression> map)
{
return new ParameterRebinder(map).Visit(exp);
}
Now you need to have a expressionvisitor to make all the expressions use one parameter
//http://blogs.msdn.com/b/meek/archive/2008/05/02/linq-to-entities-combining-predicates.aspx
public class ParameterRebinder : ExpressionVisitor
{
private readonly IDictionary<ParameterExpression, ParameterExpression> _map;
public ParameterRebinder(IDictionary<ParameterExpression, ParameterExpression> map)
{
_map = map;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (_map.ContainsKey(node))
{
return _map[node];
}
return base.VisitParameter(node);
}
}
Would use it like
var matches = retailers.Match("7", r => r.Address.Street, x => x.Address.Complement).ToList();
Warning - I checked this with linq to objects using the AsQueryable but didn't run it against EF.
You can use Linq.Dynamic to build the query.
public static IQueryable<T> Match<T>(
string searchTerm,
IQueryable<T> data,
params Expression<Func<T, string>>[] filterProperties) where T : class
{
var predicates = new List<string>();
foreach (var prop in filterProperties)
{
var lambda = prop.ToString();
var columnName = lambda.Substring(lambda.IndexOf('.') + 1);
var predicate = string.Format(
"({0} != null && {0}.ToUpper().Contains(#0))", columnName);
predicates.Add(predicate);
}
var filter = string.Join("||", predicates);
var results = data.Where(filter, searchTerm);
return results;
}
Usage.
var retailers = Match(
"asd", db.Retailers, r => r.CompanyName, r => r.TradingName);
var retailers = Match(
"asd", db.Retailers, r => r.Address.Street, r => r.Address.Complement);
Limitation.
The filter can only accept basic expression.
r => r.Name
r => r.PropA.Name
r => r.PropA.PropB.Name
Try to use Expressions like those all
http://www.codeproject.com/Articles/493917/Dynamic-Querying-with-LINQ-to-Entities-and-Express
Let's say that in my database I have the table
**Table Contact**
Id, FirstName, LastName, Phone, Email, DateCreated
1 Tom Williams 3052548623 tom#gmail.com 2013-12-21 14:51:08
etc...
I would like to enable users to search for a contact entering a string. Let's say the user enters:
tom -> TRUE
tom wil -> TRUE
wil tom -> TRUE
tom XX -> FALSE
t w 3 # -> TRUE
wil 305 -> TRUE
(True means search found customer Tom, False means it did not find it)
I will be performing this type of search among different tables in my database. It will be nice if I dont have to build the query for a specific table.
The approach I am thinking on taking is to split the search string every time I find one or more spaces. Then I will be creating n number of searches and then performing an intersect?
You could do something like that, assuming you want only search in the string properties (so your samples will work if we considere Phone as a string property).
It's of course doable with numeric properties (but gets more complicated).
A method like that in an helper static class
public static Expression<Func<T, bool>> BuildPredicateForFilter<T>(string filterString)
{
//first, split search by space, removing white spaces, and putting this to upper case
var filters = filterString.Split(new []{" "}, StringSplitOptions.RemoveEmptyEntries).Select(m => m.ToUpper());
var parameter = Expression.Parameter(typeof (T), "m");
//get string.Contains() method
var containsMethod = typeof (string).GetMethod("Contains");
//get string.ToUpper() method
var toUpperMethod = typeof (string).GetMethod("ToUpper", new Type[]{});
//find all the string properties of your class
var properties = typeof(T).GetProperties().Where(m => m.PropertyType == typeof(string));
//for all the string properties, build a "m.<PropertyName>.ToUpper() expression
var members = properties.Select(p => Expression.Call(Expression.Property(parameter, p), toUpperMethod));
Expression orExpression = null;
//build the expression
foreach (var filter in filters)
{
Expression innerExpression = null;
foreach (var member in members)
{
innerExpression = innerExpression == null
? (Expression)Expression.Call(member, containsMethod, Expression.Constant(filter))
: Expression.OrElse(innerExpression, Expression.Call(member, containsMethod, Expression.Constant(filter)));
}
orExpression = orExpression == null
? innerExpression
: Expression.AndAlso(orExpression, innerExpression);
}
return Expression.Lambda<Func<T, bool>>(orExpression, new[]{parameter});
}
usage :
var result = <yourSource>.Where(Helper.BuildPredicateForFilter<TableName>("tom XX"));
for example, with "tom XX", the orExpression will look like
((((m.FirstName.ToUpper().Contains("TOM") OrElse
m.LastName.ToUpper().Contains("TOM")) OrElse
m.Phone.ToUpper().Contains("TOM"))
OrElse m.Email.ToUpper().Contains("TOM"))
AndAlso
(((m.FirstName.ToUpper().Contains("XX") OrElse
m.LastName.ToUpper().Contains("XX")) OrElse
m.Phone.ToUpper().Contains("XX")) OrElse
m.Email.ToUpper().Contains("XX")))
EDIT
or you could change the method to
public static IQueryable<T> FilterFor(this IQueryable<T> queryable, string filterString) {
//same
var predicate = Expression.Lambda<Func<T, bool>>(orExpression, new[]{parameter});
return queryable.Where(predicate);
}
then usage would simply be
<yourSource>.FilterFor("tom XX");
So what we're looking to do here is search through all of the fields in a type for a given value, doing a Contains search. We can write a method to do this.
First we'll need to use a PredicateBuilder, as we'll be dynamically generating a number of expressions that we want to OR together. Here is my definition of a PredicateBuilder capable of doing that:
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 secondBody = expr2.Body.Replace(expr2.Parameters[0], expr1.Parameters[0]);
return Expression.Lambda<Func<T, bool>>
(Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var secondBody = expr2.Body.Replace(expr2.Parameters[0], expr1.Parameters[0]);
return Expression.Lambda<Func<T, bool>>
(Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
}
}
This uses the following helper method/class to replace all instances of one Expression with another:
internal class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(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 Replace(this Expression expression,
Expression searchEx, Expression replaceEx)
{
return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
Another tool that we'll use to solve this problem is a Compose method. It will take one expression, then another expression that takes as input the output of another, and produces a new expression that takes the input of the first and produces the output of the last.
public static Expression<Func<TFirstParam, TResult>>
Compose<TFirstParam, TIntermediate, TResult>(
this Expression<Func<TFirstParam, TIntermediate>> first,
Expression<Func<TIntermediate, TResult>> second)
{
var param = Expression.Parameter(typeof(TFirstParam), "param");
var newFirst = first.Body.Replace(first.Parameters[0], param);
var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}
Thanks to all of these tools, what's left is actually quite straightforward. We'll accept a query, a string to search for, and a series of selectors, each selecting out a field to search through. Then we initialize a filter, go through every single selector, use Compose to turn each selector into a predicate that performs a Contains check on the relevant search text, and then ORs that to the existing filter.
public static IQueryable<T> AnyFieldContains<T>(
this IQueryable<T> query,
string searchText,
params Expression<Func<T, string>>[] fieldSelectors)
{
var filter = PredicateBuilder.False<T>();
foreach (var selector in fieldSelectors)
{
filter = filter.Or(selector.Compose(
value => value.Contains(searchText)));
}
return query.Where(filter);
}
Now that we have all of this we can split the input that you have, and for each of those expressions we can call this method. Then you simply need to supply selectors for the fields that need to be searched through:
IQueryable<Foo> query = db.Foo;
string searchText = "wil tom";
var searchExpressions = searchText.Split(' ');
foreach (var expression in searchExpressions)
{
query = query.AnyFieldContains(expression,
foo => foo.FirstName,
foo => foo.LastName,
foo => foo.Phone);
}
var result = query.Any();
If you're really sure that you want to search every field (and I'm not sure if you are, it's likely many tables will have fields that shouldn't be searched, or have fields that will need some sort of work on your end to transform them into an appropriate string worth searching), then you can use reflection to generate all of the selectors, rather than typing out explicitly what you want to have searched. We can simply create an additional overload such that if no selectors are provided it will use "everything":
public static IQueryable<T> AnyFieldContains<T>(
this IQueryable<T> query,
string searchText)
{
return AnyFieldContains(query, searchText,
typeof(T).GetProperties()
.Select(prop => CreateSelector<T>(prop))
.ToArray());
}
private static Expression<Func<T, string>> CreateSelector<T>(PropertyInfo prop)
{
var param = Expression.Parameter(typeof(T));
Expression body = Expression.Property(param, prop);
if (prop.PropertyType == typeof(decimal?))
body = Expression.Call(body, typeof(SqlFunctions)
.GetMethod("StringConvert", new[] { typeof(decimal?) }));
else if (prop.PropertyType == typeof(double?))
body = Expression.Call(body, typeof(SqlFunctions)
.GetMethod("StringConvert", new[] { typeof(double?) }));
return Expression.Lambda<Func<T, string>>(body, param);
}
So what I am trying to do is use expression trees to apply a predicate to each value in a collection (read map or list.All(predicate)). It appears that I am not getting the input parameter to the predicate bound to the value supplied by All, and I'm a little stuck. Here is the code (using linqpad) that I am working with::
public class SomeType
{
public IEnumerable<bool> Collection { get; set; }
}
void Main()
{
var list = new SomeType {
Collection = new List<bool> { true, true, true }
};
var functor = Compiler((SomeType t) => t.Collection, (bool x) => x);
functor(list).Dump();
}
MethodInfo FindMethod<TInput>(Type location, string name)
{
var handle = location
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(method => method.Name == name).First();
return handle.MakeGenericMethod(typeof(TInput));
}
Predicate<TObject> Compiler<TObject, TProperty>(
Expression<Func<TObject, IEnumerable<TProperty>>> selector,
Expression<Predicate<TProperty>> predicate)
{
var query = FindMethod<TProperty>(typeof(Enumerable), "All");
var expression = Expression.Call(query,
new Expression[] {
Expression.Invoke(selector, selector.Parameters),
Expression.Lambda<Func<TProperty, bool>>(predicate.Body,
Expression.Parameter(typeof(TProperty))),
});
return Expression.Lambda<Predicate<TObject>>(expression,
selector.Parameters).Compile();
}
Thanks and sorry if this was answered in another question (I looked for a while).
This does work, but I had to change the Predicate<TObject> to Func<TObject, bool>. If you want I can try to change it back.
static Predicate<TObject> Compiler<TObject, TProperty>(
Expression<Func<TObject, IEnumerable<TProperty>>> selector,
Expression<Func<TProperty, bool>> predicate)
{
var query = FindMethod<TProperty>(typeof(Enumerable), "All");
var expression = Expression.Call(
query,
Expression.Invoke(selector, selector.Parameters),
predicate);
return Expression
.Lambda<Predicate<TObject>>(expression, selector.Parameters)
.Compile();
}
5 minutes later... And if you really want to use Predicate<TObject>...
static Predicate<TObject> Compiler<TObject, TProperty>(
Expression<Func<TObject, IEnumerable<TProperty>>> selector,
Expression<Predicate<TProperty>> predicate)
{
var query = FindMethod<TProperty>(typeof(Enumerable), "All");
var predicateAsFunc = Expression.Lambda<Func<TProperty, bool>>(
predicate.Body,
predicate.Parameters);
var expression = Expression.Call(
query,
Expression.Invoke(selector, selector.Parameters),
predicateAsFunc);
return Expression
.Lambda<Predicate<TObject>>(expression, selector.Parameters)
.Compile();
}