
🧪 LINQ on Steroids: Dynamic Filtering with Expression Trees in .NET
Learn how to create a dynamic, generic IQueryable filter in .NET using expression trees. Cut repetitive LINQ code and build flexible filters for EF Core.
A reusable and dynamic way to filter IQueryable<T>
collections in .NET and Entity Framework using expression trees.
In the endless world of .NET tasks, I doubt there is one more annoying than filtering.
The properties may be endless and the code so dull and boring. Yes, there may be ways to automate it or at least structure it to create cleaner code, but let’s be honest here… there is never time.
So, this is my story of how I spent 10 hours automating a task that could have taken me 15 minutes tops.
🧠 The Idea
The goal was simple (or so I thought):
- I would create a generic class with one method that returns an
IQueryable<T>
and receives a filter object. - Inside the method, I’d compare the filter and the database model to see what matches and what doesn’t.
Sounds straightforward, right?
📚 The Full Plan Looked Like This:
- Get the property of the database model that has the same name as the property of the filter
- Skip properties not present in the model (maybe they’re calculated or UI-only)
- Get the runtime value of each filter property
- If the property is a
DateTime
, skip it if it’snull
orDateTime.MinValue
- For strings, apply
Trim()
andToUpper()
before comparing - Build the full filtering
Expression<Func<T, bool>>
dynamically - Optionally accept an external condition to merge into the final expression tree
✅ Why Not Just Chain .Where()
?
Sure, I could’ve just passed the filter
and applied a .Where()
outside the method. But that would mean two Where()
calls, which can lead to unnecessary SQL nesting and subqueries in EF Core.
Instead, I chose to accept an optional Expression<Func<T, bool>>
and merge it into the expression tree, so the entire filtering logic stays in a single Where()
clause.
🧰 The Code: FilterQueryBuilder
public static class FilterQueryBuilder { //SHEMBULL additionalFilter: Expression<Func<T, bool>> additionalFilter = ((T p) => p.CreatedDate.Date == filter.SignedDate.Date); public static IQueryable<T> ApplyFilter<T>(IQueryable<T> query, object filter, Expression<Func<T, bool>> additionalFilter = null) { if (filter == null) { return query; } //getting all properties in the filter object var filterProperties = filter.GetType().GetProperties(); //lets create the x=> part of the lambda ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); Expression expression = Expression.Constant(true); foreach (var property in filterProperties) { //we get the property of the database model that has the same name as the property of the filter var modelProperty = typeof(T).GetProperty(property.Name); //we check what property is not present in the model but it exists on the filter and we skip it //Why? Maybe there are some properties that are not directly stored in the database but they are calculated in a specific way (smth we will handle later) if (modelProperty == null) { continue; } //we get the value of the filter property at runtime //the second parameter is for indexed properties ex. arrays. Since we are dealing with regular properties pass null var propertyValue = property.GetValue(filter, null); //checking the case where the property type is Datetime if (property.PropertyType == typeof(DateTime)) { //sometimes the datetime is not passed as null(even if nullable), but as the default value of DateTime.MinValue //DateTime.MinValue == 01/01/0001 00:00:00 if (propertyValue == null || (propertyValue.HasValue && (DateTime)propertyValue.Value == DateTime.MinValue)) continue; //here is created the part x=>x.ModelProperty that takes the exact property we are searching for in the object var memberExpression = Expression.Property(parameter, modelProperty); //here we get the value that should be compared var filterDate = ((DateTime)propertyValue!); //here the expression is finalized with the check == filterDate,the value of the filter, with the type filterDate.Date to not check the time BinaryExpression dateEqualityExpression = Expression.Equal( Expression.Property(memberExpression, nameof(DateTime.Date)), Expression.Constant(filterDate.Date) ); //this adds the condition to the filter expression we are creating with an &&. //So the expresion turns in what we had in that moment && the date check expression = Expression.AndAlso(expression, dateEqualityExpression); } else if (propertyValue != null) { //Here we add the checks for other property types var propertyType = property.PropertyType; var memberExpression = Expression.Property(parameter, property.Name); var filterValue = Expression.Constant(propertyValue); BinaryExpression equalityExpression = null; // for properties we cannot use the toupper and trim methods the same as with constants. We have to get them and then make a call. if (propertyType == typeof(string) && propertyValue is string stringValue && !string.IsNullOrEmpty(stringValue) && memberExpression.Type == typeof(String)) { propertyValue = stringValue.Trim().ToUpper(); var toUpperMethod = typeof(string).GetMethod("ToUpper", Type.EmptyTypes); var trimMethod = typeof(string).GetMethod("Trim", Type.EmptyTypes); var toUpperCall = Expression.Call(memberExpression, toUpperMethod!); var trimCall = Expression.Call(toUpperCall, trimMethod!); //creating the expression equalityExpression = Expression.Equal(trimCall, Expression.Constant(propertyValue)); expression = Expression.AndAlso(expression, equalityExpression); } else { //creating the expression equalityExpression = Expression.Equal(memberExpression, filterValue); expression = Expression.AndAlso(expression, equalityExpression); } } } //check if there is an additional filter if (additionalFilter != null) { //attach with && through AndAlso to the created expression var additionalFilterExpression = Expression.Invoke(Expression.Constant(additionalFilter), parameter); expression = Expression.AndAlso(expression, additionalFilterExpression); } var lambda = Expression.Lambda<Func<T, bool>>(expression, parameter); var modifiedLambda = ModifyLambda(lambda); return query.Where(modifiedLambda); } //this method adds the x. before the properties that are in the database model, because we dont need it with constants static Expression<Func<T, bool>> ModifyLambda<T>(Expression<Func<T, bool>> originalLambda) { if (IsTrueConstant(originalLambda)) { return Expression.Lambda<Func<T, bool>>(Expression.Constant(false), originalLambda.Parameters); } return originalLambda; } static bool IsTrueConstant<T>(Expression<Func<T, bool>> lambda) { // Check if the lambda body is a constant expression with value true if (lambda.Body is ConstantExpression constant && constant.Value is bool constantValue) { return constantValue == true; } return false; } }
🧪 Example Usage
1. Define a Filter Class
public class UserFilter { public string Name { get; set; } public DateTime? CreatedDate { get; set; } }
2. Use the Filter in a Repository or Service
public async Task<List<User>> GetFilteredUsersAsync(UserFilter filter) { var query = _context.Users.AsQueryable(); query = FilterQueryBuilder.ApplyFilter(query, filter); return await query.ToListAsync(); }
3. Optional: Add Extra Filtering Logic
Expression<Func<User, bool>> extraFilter = u => u.IsActive; query = FilterQueryBuilder.ApplyFilter(query, filter, extraFilter);