🧪 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’s null or DateTime.MinValue
  • For strings, apply Trim() and ToUpper() 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);

.NET Expression Trees docs

LINQ Tutorial – Microsoft

Leave a Reply

Your email address will not be published. Required fields are marked *