Reputation: 11
I'm trying to create a dynamic LINQ query method which receives a List< KeyValuePair< string, string>> to apply the conditions.
Basically, for each element of the List< KeyValuePair (...)>, I want to add a condition to my query.
This project is being developed using the repository-pattern that is very popular in .NET Core.
In SQL I've managed to get the query right:
SELECT * FROM events e
WHERE (e.store = "CO" && e.brand = "YL") || (e.store = "VA" && e.brand = "CD");
The elements of the List<KeyValuePair<string, string>>
look like this:
List<KeyValuePair<string, string>> filters = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("CO","YL"),
new KeyValuePair<string, string>("VA", "CD")
};
Now I need a method that iterates through my List<KeyValuePair<string, string>>
and builds a Dynamic LINQ query for every item of the list. If I have a List that has 5 elements I want a query with 5 conditions with OR between them like this:
SELECT * FROM events e
WHERE
(e.store = "CO" && e.brand = "YL") ||
(e.store = "VA" && e.brand = "CD") ||
(e.store = "FP" && e.brand = "CH") ||
(e.store = "MC" && e.brand = "AR") ||
(e.store = "AB" && e.brand = "CH");
This is my attempt:
var query = Query();
foreach (var item in filters)
{
query = query.Where(e => e.Store.Equals(i.Key) && e.Brand.Equals(i.Value));
}
var results = await query.ToListAsync(ct);
But this way I can't apply the OR operator between the conditions. Does anyone have any idea of how it can be done?
Thank you.
Upvotes: 1
Views: 3163
Reputation: 182
This question really interested me, and I tried to compile something working without using third-party libraries. Next, I will present pieces of code that together gave the desired result. But most likely this is all for educational purposes.
First, we need function and class taken from here: https://www.c-sharpcorner.com/UploadFile/c42694/dynamic-query-in-linq-using-predicate-builder/
static Expression<T> Compose<T>(Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
{
// zip parameters (map from parameters of second to parameters of first)
var map = first.Parameters
.Select((f, i) => new { f, s = second.Parameters[i] })
.ToDictionary(p => p.s, p => p.f);
// replace parameters in the second lambda expression with the parameters in the first
var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
// create a merged lambda expression with parameters from the first expression
return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
}
class ParameterRebinder : ExpressionVisitor {
readonly Dictionary<ParameterExpression, ParameterExpression> map;
ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression p)
{
ParameterExpression replacement;
if (map.TryGetValue(p, out replacement))
{
p = replacement;
}
return base.VisitParameter(p);
}
}
Then we can do something like this (I used tuples because they are more in line with the idea):
var tup = new List<Tuple<string, string>> {
new Tuple<string, string>("CO", "YL"),
new Tuple<string, string>("VA", "CD")
};
Expression<Func<YOUR_TYPE_HERE, bool>> baseFunc = t => false;
foreach (var a in tup)
{
Expression<Func<YOUR_TYPE_HERE, bool>> addFunc = t => t.store == a.Item1 && t.brand == a.Item2;
baseFunc = Compose(baseFunc, addFunc, Expression.OrElse);
}
var res = _context.YOUR_ENTITY_NAME.Where(baseFunc).ToList();
I checked that it is executed in one query and evaluate on the database side.
UPD: If you wanna be more performance and want use a bit sql:
var res = context.YOUR_ENTITY_NAME.FromSql("SELECT * FROM YOUR_ENTITY_NAME WHERE (...and...) or (...and...)").ToList();
You can generate "where" part manually and put it as string at the end on "SELECT * FROM YOUR_ENTITY_NAME WHERE"
. But be carefully with injections. Here you need to use parameters.
Upvotes: 0
Reputation: 178
Looks like your easiest bet would be to use Dynamic Linq, since that allows you to create your "(A && B) || (C && D)" as strings at runtime and use them in a Where clause. Alternatively you could build expression trees; see for instance http://stackoverflow.com/questions/6295926/how-build-lambda-expression-tree-with-multiple-conditions
Upvotes: 2
Reputation: 555
I am assuming you have a mistake in your code. e.Store.Equals(i.Key)
should be e.Store.Equals(item.Key)
. Correct me if I'm wrong.
This should do the job.
query = query.Where(e => filters.Any(f => e.Store.Equals(filter.Key) && e.Brand.Equals(filter.Value)));
Note that this query may be executed on the client and not on the database. See here: https://learn.microsoft.com/en-us/ef/core/querying/client-eval
Meaning that the query will work up until EF Core 2.2, but not with EF Core 3.0. See breaking changes in EF Core 3.0 here: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes
Upvotes: 0