I'm trying to create a way to allow users of my app to create their own filter views of data. To do that I'm composing a list of Key-Operator-Value pairs which I then try, after multiple steps, to parse into an expression to use with a Where
call. So far, I can get to a point where I have the expressions created, but I can't figure out how to merge them into a single expression.
Could someone point me in the right direction on how to do so? From reading similar posts online, I've settled on trying to do it using the LINQKit PredicateBuilder
, but all I'm getting back is the initial starting predicate. Here's the code I have so far, it is by no means final, I'm just prototyping it in LINQPad:
void Main() {
// 1. Create form post object
var query = new QueryEdit {
Filters = new List<Filter> {
new Filter {
Id = 1,
Key = "Name",
Operator = "=",
Value = "New York"
},
new Filter {
Id = 2,
Key = "Name",
Operator = "!=",
Value = "Boston"
},
new Filter {
Id = 3,
Key = "Name",
Operator = "=",
Value = "Washington"
}
},
FilterClause = "1 AND 3 OR 2 OR 4"
};
// 2. Compose filter groups
var filterGroups = GetFilterGroups(query);
// 3. Compose expression groups
var expressionGroups = GetExpressionGroups<Region>(filterGroups);
expressionGroups.Dump();
// 4. Compose full expression
var expression = GetExpression(expressionGroups);
expression.Dump();
// 5. Serialize expression to blob
// 6. Deserialize blob to expression
// 7. Execute expression against db
}
static IEnumerable<FilterGroup> GetFilterGroups(
QueryEdit query) {
// The parentheses are meaningless, so they need to be removed.
// The groups are all conditions with ANDs, separated by ORs.
return query.FilterClause.Replace(")", string.Empty).Replace("(", string.Empty).Split(new[] { "OR" }, StringSplitOptions.None).Select(
fc => new FilterGroup {
Filters = fc.Split(new[] { "AND" }, StringSplitOptions.None).Select(
i => {
var id = Convert.ToByte(i);
return query.Filters.SingleOrDefault(
f => f.Id == id);
}).Where(
f => f != null).OrderBy(
f => f.Id)
}).Where(
fg => fg.Filters.Any());
}
static IEnumerable<IEnumerable<Expression<Func<T, bool>>>> GetExpressionGroups<T>(
IEnumerable<FilterGroup> filterGroups) {
var type = typeof(T);
var parameter = Expression.Parameter(type);
return filterGroups.Select(
fg => {
return fg.Filters.Select(
f => {
var left = Expression.Property(parameter, type.GetProperty(f.Key));
var right = Expression.Constant(f.Value);
var body = GetExpressionBody(f.Operator, left, right);
if (body == null) {
return null;
}
return Expression.Lambda<Func<T, bool>>(body, parameter);
}).Where(
e => e != null);
});
}
static object GetExpression<T>(
IEnumerable<IEnumerable<Expression<Func<T, bool>>>> expressionGroups) {
var predicate = PredicateBuilder.True<T>();
foreach (var expressionGroup in expressionGroups) {
var expression = GetPredicateForGroup<T>(expressionGroup);
predicate.Or(expression);
}
return predicate;
}
static Expression<Func<T, bool>> GetPredicateForGroup<T>(
IEnumerable<Expression<Func<T, bool>>> expressionGroup) {
var predicate = PredicateBuilder.True<T>();
foreach (var expression in expressionGroup) {
predicate.And(expression);
}
return predicate;
}
static Expression GetExpressionBody(
string @operator,
Expression left,
Expression right) {
switch (@operator) {
case "=":
return Expression.Equal(left, right);
case "!=":
return Expression.NotEqual(left, right);
case ">":
return Expression.GreaterThan(left, right);
case ">=":
return Expression.GreaterThanOrEqual(left, right);
case "<":
return Expression.LessThan(left, right);
case "<=":
return Expression.LessThanOrEqual(left, right);
default:
return null;
}
}
sealed class QueryEdit {
public IEnumerable<Filter> Filters { get; set; }
public string FilterClause { get; set; }
public int Id { get; set; }
public string Name { get; set; }
}
sealed class Filter {
public byte Id { get; set; }
public string Key { get; set; }
public string Operator { get; set; }
public object Value { get; set; }
}
sealed class FilterGroup {
public IEnumerable<Filter> Filters { get; set; }
}
I'm also open to other suggestions on how to accomplish this, or overall improvements. Thanks in advance!
Try
predicate = predicate.Or(expression)
and
predicate = predicate.And(expression)