Skip to content

Instantly share code, notes, and snippets.

@afreeland
Last active June 13, 2025 19:00
Show Gist options
  • Save afreeland/6733381 to your computer and use it in GitHub Desktop.
Save afreeland/6733381 to your computer and use it in GitHub Desktop.
C#: LINQ Expression Builder dynamic where clause based on filters
public class ExpressionBuilder
{
// Define some of our default filtering options
private static MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
private static MethodInfo startsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
private static MethodInfo endsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
public static Expression<Func<T, bool>> GetExpression<T>(List<GridHelper.Filter> filters)
{
// No filters passed in #KickIT
if (filters.Count == 0)
return null;
// Create the parameter for the ObjectType (typically the 'x' in your expression (x => 'x')
// The "parm" string is used strictly for debugging purposes
ParameterExpression param = Expression.Parameter(typeof(T), "parm");
// Store the result of a calculated Expression
Expression exp = null;
if (filters.Count == 1)
exp = GetExpression<T>(param, filters[0]); // Create expression from a single instance
else if (filters.Count == 2)
exp = GetExpression<T>(param, filters[0], filters[1]); // Create expression that utilizes AndAlso mentality
else
{
// Loop through filters until we have created an expression for each
while (filters.Count > 0)
{
// Grab initial filters remaining in our List
var f1 = filters[0];
var f2 = filters[1];
// Check if we have already set our Expression
if (exp == null)
exp = GetExpression<T>(param, filters[0], filters[1]); // First iteration through our filters
else
exp = Expression.AndAlso(exp, GetExpression<T>(param, filters[0], filters[1])); // Add to our existing expression
filters.Remove(f1);
filters.Remove(f2);
// Odd number, handle this seperately
if (filters.Count == 1)
{
// Pass in our existing expression and our newly created expression from our last remaining filter
exp = Expression.AndAlso(exp, GetExpression<T>(param, filters[0]));
// Remove filter to break out of while loop
filters.RemoveAt(0);
}
}
}
return Expression.Lambda<Func<T, bool>>(exp, param);
}
private static Expression GetExpression<T>(ParameterExpression param, GridHelper.Filter filter)
{
// The member you want to evaluate (x => x.FirstName)
MemberExpression member = Expression.Property(param, filter.PropertyName);
// The value you want to evaluate
ConstantExpression constant = Expression.Constant(filter.Value);
// Determine how we want to apply the expression
switch (filter.Operator)
{
case GridHelper.Operator.Equals:
return Expression.Equal(member, constant);
case GridHelper.Operator.Contains:
return Expression.Call(member, containsMethod, constant);
case GridHelper.Operator.GreaterThan:
return Expression.GreaterThan(member, constant);
case GridHelper.Operator.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case GridHelper.Operator.LessThan:
return Expression.LessThan(member, constant);
case GridHelper.Operator.LessThanOrEqualTo:
return Expression.LessThanOrEqual(member, constant);
case GridHelper.Operator.StartsWith:
return Expression.Call(member, startsWithMethod, constant);
case GridHelper.Operator.EndsWith:
return Expression.Call(member, endsWithMethod, constant);
}
return null;
}
private static BinaryExpression GetExpression<T>(ParameterExpression param, GridHelper.Filter filter1, GridHelper.Filter filter2)
{
Expression result1 = GetExpression<T>(param, filter1);
Expression result2 = GetExpression<T>(param, filter2);
return Expression.AndAlso(result1, result2);
}
}
public ActionResult Grid()
{
//....
//Implementing ExpressionBuilder
Expression<Func<Contact, bool>> deleg = ExpressionBuilder.GetExpression<Contact>(filters);
_contacts = _contacts.Where(deleg);
//.....
}
// Filter class
public class GridHelper
{
public enum Operator
{
Contains,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqualTo,
StartsWith,
EndsWith,
Equals,
NotEqual
}
public class Filter
{
public string PropertyName { get; set; }
public string Value { get; set; }
private Operator _op = Operator.Contains;
public Operator Operator
{
get
{
return _op;
}
set
{
_op = value;
}
}
}
public class Filters : List<GridHelper.Filter>
{
public Filters(params string[] args)
{
HttpContext context = System.Web.HttpContext.Current;
foreach (var arg in args)
{
this.Add(arg, context.Request.Form[arg] ?? "");
}
}
public void Add(string Name, string Value = "")
{
this.Add(new Filter(){ PropertyName = Name, Value = Value});
}
}
}
}
@gvii
Copy link

gvii commented Jul 22, 2015

Hi, I am trying to use this code and I would like to make the "equals" operation case insensitive.

case GridHelper.Operator.Equals:
                return Expression.Equal(member, constant);

Would you pls. let me know how would I go about it?

Thanks in advance

@emrebostan
Copy link

emrebostan commented Dec 18, 2017

i love this code

@iloshkakartochka
Copy link

Contains, does not work if there is case sensitive data. So i made a slight change to contains, in case anyone needs it. Basically, use indexof

var pi = param.Type.GetProperty(filter.PropertyName);
var propertyAccess = Expression.MakeMemberAccess(param, pi);
var indexOf = Expression.Call(propertyAccess, "IndexOf", null, Expression.Constant(constant.Value, typeof(string)), Expression.Constant(StringComparison.InvariantCultureIgnoreCase));
return Expression.GreaterThanOrEqual(indexOf, Expression.Constant(0));

@arunzer0
Copy link

Really Such a nice code

@praveenravanang
Copy link

This doesn't work for Nullable Data types like datetime,int for operators <, > >=,Please suggest

@thummartechnolab
Copy link

How can we handle nullable column.?
age column is nullable and i want to search LIKE operator how can we search?
Example: Select * from User where age like '%10%'

@s-nandra
Copy link

s-nandra commented Mar 1, 2024

Contains workaround for EF .net postgres:

Declare
private static MethodInfo? containsMethod = typeof(string).GetMethod("Contains", new Type[] { typeof(string) });
private static MethodInfo? toLowerMethod = typeof(string).GetMethod("ToLower", new Type[0]);

 Then 
 
  case FilterOperationEnum.Contains:
        
             var memeberToLower = Expression.Call(member, toLowerMethod);
             return Expression.Call(memeberToLower, containsMethod, actualValue);
        

 break;

@j-lath
Copy link

j-lath commented Jun 13, 2025

@thummartechnolab
I recently had the same issue you were facing. To handle NULLs, change lines 63 & 64 to:

       // make sure we can convert from string for comparison
        var propertyType = ((PropertyInfo)member.Member).PropertyType;
        var converter = TypeDescriptor.GetConverter(propertyType);

        if (!converter.CanConvertFrom(typeof(string)))
            throw new NotSupportedException();

        // The value that you want to evaluate, converted
        var convertedFilterValue = converter.ConvertFromInvariantString(filter.Value);

        // The value you want to evaluate
        ConstantExpression constant = Expression.Constant(convertedFilterValue);
        //ConstantExpression constant = Expression.Constant(filter.Value);

        UnaryExpression valueExpression = Expression.Convert(constant, propertyType);

And then, when you do your Expression.Equal, etc, use valueExpression instead of constant. Effectively, make sure that your incoming filter.Value, which is currently string, can be converted to the type of the field in the target data source / database; and if so, build up a conversion expression and use that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment