I will preface this with I'm actively searching for the solution to this problem but figured I might short cut some research and development time if someone here on stack has already figured this out. (I have found nothing online so here goes)
We have a case in an application framework we are building where we need the capability to take in a set of Predicates (List<Expression<Func<T,bool>>>
) and parse it in a search framework.
Right now we have the capability to filter in this way being that:
//Assume predicates is passed as a method argument.
// of List<Expression<Func<T,bool>>>
//Assume user is passed in as a method argument.
//Assume FilterToUserAccess is a custom extension method that restricts the dataset
// to access restrictions.
var query = _dbContext.Set<EntityType>()
.FilterToUserAccess(user);
foreach(var p in predicates){
query = query.Where(p);
}
return p.ToList();
The reason we need to do this is for scale-ability of filterable objects. However for a quick search this is not possible given the built in capabilities of EF. What I need to be able to do is:
Object A (lets pretend it's a race car) and we want to search make, model, team, and driver in a quick search box. So if I enter "Earnhardt", it would search all race car entity properties being make, model, team, and driver. I would end up with all the DEI cars as well as Dale Jr. I would like to use the same approach so we can configure a searchable entity and reflect the search configuration on application start. I would ideally like to make some way of having the query look similar to this:
//Assume predicates is passed as a method argument.
// of List<Expression<Func<T,bool>>>
//Assume user is passed in as a method argument.
//Assume FilterToUserAccess is a custom extension method that restricts the dataset
// to access restrictions.
var query = _dbContext.Set<EntityType>()
.FilterToUserAccess(user);
foreach(var p in predicates){
query = query.Or(p);
}
return p.ToList();
I realize I can do:
_dbContext.Set<EntityType>().Where(predicate1 || predicate2 || predicate3)
However this will not work for the approach we want to take to solve this problem. Ideally an admin for one of our client sites would be able to go in and configure an additional search term with a single click to be included in any and all quick searches for that entity type like we can currently pull off with Filters which use the standard .Where(...)
"and" chaining logic.
First solution was a bust, however with some more digging there is an incredibly simple solution, verified and works.
Step 1: install the NuGet package for LinqKit.
Step 2: Enjoy the code below
using (ISampleRepository repo = new SampleRepository())
{
var predicates = new List<Expression<Func<Customer,bool>>>(){
(x => x.FirstName.Contains(searchValue)),
(x => x.LastName.Contains(searchValue))
};
var lambda = PredicateBuilder.False<Customer>();
lambda = predicates.Aggregate(lambda, (current, p) => current.Or(p).Expand());
var query = repo.QueryCustomers().AsExpandable().Include(x => x.Phones).Where(lambda);
return query.Take(500)
.ToList()
.Select(x => x.ToDTO())
.ToList();
}
This is just the spike sample but doing the same thing with a method taking in ->
List<T> QuickSearch<T>(string input) ...
Will be able to use the same approach. You have a collection of predicates still in Expression form passed in, then you use the predicate builder tricks to pull the query off. Then using the AsExpandable() allows you to execute the combined predicate created using the predicate builder.
Hopefully this is helpful to more than just me, but this is the solution I'm going with as it's quite a bit less code. Allows you to build your predicates elsewhere... and still combine them in an "OR" statement after the fact.
As Ladislav says, you will need to dynamically generate your LINQ expressions. Here is a simple example of a program that dynamically builds a predicate for a collection of integers:
class Program {
static void Main(string[] args) {
// Retreive your data source
List<int> numbers = new List<int>() { 0, 10, 20, 30, 40, 50, 60 };
// Create a collection of predicates that you would like to chain together.
ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "x");
List<Expression> predicates = new List<Expression>();
// x >= 50
predicates.Add(Expression.GreaterThanOrEqual(parameterExpression, Expression.Constant(50)));
// x <= 20
predicates.Add(Expression.LessThanOrEqual(parameterExpression, Expression.Constant(20)));
// Build a single predicate by chaining individual predicates together in an OR fashion
Expression whereFilter = Expression.Constant(false); // Use false a base expression in OR statements
foreach (var predicate in predicates) {
whereFilter = Expression.OrElse(whereFilter, predicate);
}
// Once the expressions have been chained, create a lambda to represent the whole predicate
// x => (x >= 50) || (x <= 20)
Expression<Func<int, bool>> whereLambda =
(Expression<Func<int, bool>>)Expression.Lambda(whereFilter,
new List<ParameterExpression>() { parameterExpression });
// To use an expression directly, the datasource must be an IQueryable
// Since I am using List<T> I must call AsQueryable. This is not necessary
// if your collection is already IQueryable, like in Entity Framework.
var results = numbers.AsQueryable().Where(whereLambda);
}
}
Essentially all I do here is create several boolean statments (x >= 50) and (x <= 20) and place them in a collection. Then by looping through that collection, I take each statement and OR it to the last one. The result is a series of boolean statements all linked together by OR. I then wrap that statement in a Lambda expression so that it can be consumed by IQueryable.Where
and pass it to my queryable collection. The results are a filtered set of integers from my full set.
LINQ Expressions can be confusing to say the least, but they are incredibly powerful and worthwhile to know. Please let me know if there's anything I can do to help make more sense of this example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With