Quite convinced that your data model is too generic. It will hurt you in terms of program clarity and performance.
But let's go with it for this answer, which I took as a challenge in expression building. The goal is to get a nice queryable that honors the filters on the data server side.
Here's the data model I used, which I think closely matches yours:
public sealed class Document { public int Id { get; set; } // ... public ICollection<DocumentField> Fields { get; set; } } public sealed class DocumentField { public int Id { get; set; } public int DocumentId { get; set; } public string StringValue { get; set; } public float? FloatValue { get; set; } // more typed vales here }
First, I implement conveniance functions to create predicates for individual fields of individual field types:
public static class DocumentExtensions { private static readonly PropertyInfo _piFieldId = (PropertyInfo)((MemberExpression)((Expression<Func<DocumentField, int>>)(f => f.Id)).Body).Member; private static Expression<Func<DocumentField, bool>> FieldPredicate<T>(int fieldId, T value, Expression<Func<DocumentField, T>> fieldAccessor) { var pField = fieldAccessor.Parameters[0]; var xEqualId = Expression.Equal(Expression.Property(pField, _piFieldId), Expression.Constant(fieldId)); var xEqualValue = Expression.Equal(fieldAccessor.Body, Expression.Constant(value, typeof(T))); return Expression.Lambda<Func<DocumentField, bool>>(Expression.AndAlso(xEqualId, xEqualValue), pField); } /// <summary> /// f => f.<see cref="DocumentField.Id"/> == <paramref name="fieldId"/> && f.<see cref="DocumentField.StringValue"/> == <paramref name="value"/>. /// </summary> public static Expression<Func<DocumentField, bool>> FieldPredicate(int fieldId, string value) => FieldPredicate(fieldId, value, f => f.StringValue); /// <summary> /// f => f.<see cref="DocumentField.Id"/> == <paramref name="fieldId"/> && f.<see cref="DocumentField.FloatValue"/> == <paramref name="value"/>. /// </summary> public static Expression<Func<DocumentField, bool>> FieldPredicate(int fieldId, float? value) => FieldPredicate(fieldId, value, f => f.FloatValue); // more overloads here }
Usage:
var fieldPredicates = new[] { DocumentExtensions.FieldPredicate(32, "CET20533"), // f => f.Id == 32 && f.StringValue == "CET20533" DocumentExtensions.FieldPredicate(16, "882341"), DocumentExtensions.FieldPredicate(12, 101746F) // f => f.Id == 12 && f.FloatValue == 101746F };
Second, I implement an extension method HavingAllFields(also in DocumentExtensions) that creates an IQueryable<Document> where all of the field predicates are satisfied by at least one field:
private static readonly MethodInfo _miAnyWhere = ((MethodCallExpression)((Expression<Func<IEnumerable<DocumentField>, bool>>)(fields => fields.Any(f => false))).Body).Method; private static readonly Expression<Func<Document, IEnumerable<DocumentField>>> _fieldsAccessor = doc => doc.Fields; /// <summary> /// <paramref name="documents"/>.Where(doc => doc.Fields.Any(<paramref name="fieldPredicates"/>[0]) && ... ) /// </summary> public static IQueryable<Document> HavingAllFields(this IQueryable<Document> documents, IEnumerable<Expression<Func<DocumentField, bool>>> fieldPredicates) { using (var e = fieldPredicates.GetEnumerator()) { if (!e.MoveNext()) return documents; Expression predicateBody = Expression.Call(_miAnyWhere, _fieldsAccessor.Body, e.Current); while (e.MoveNext()) predicateBody = Expression.AndAlso(predicateBody, Expression.Call(_miAnyWhere, _fieldsAccessor.Body, e.Current)); var predicate = Expression.Lambda<Func<Document, bool>>(predicateBody, _fieldsAccessor.Parameters); return documents.Where(predicate); } }
Test:
var documents = (new[] { new Document { Id = 1, Fields = new[] { new DocumentField { Id = 32, StringValue = "CET20533" }, new DocumentField { Id = 16, StringValue = "882341" }, new DocumentField { Id = 12, FloatValue = 101746F }, } }, new Document { Id = 2, Fields = new[] { new DocumentField { Id = 32, StringValue = "Bla" }, new DocumentField { Id = 16, StringValue = "882341" }, new DocumentField { Id = 12, FloatValue = 101746F }, } } }).AsQueryable(); var matches = documents.HavingAllFields(fieldPredicates).ToList();
Matches document 1, but not 2.
db.DocumentsisIQueryable<T>, the second approach with chaining multipleWhereshould work - I don't see any concurrency issues there since it's just building a query. Of course I'm assuming you use==in both places (=is being a typo).foreach(filter in filters) query = query.Where(GetExpr(fiter));