Search code examples
c#linqdynamiccase-insensitivestrong-typing

how to build case insensitive strong typed LINQ query in c#?


I try to build extension method for IQuerable like this:

public static IQueryable<T> FilterByString<T>(this IQueryable<T> query, 
  Expression<Func<T, string>> propertySelector, 
  StringOperator operand, 
  string value)

that will generalize something like this:

query.Where(o => o.Name.ToLower().StartsWith(filterObject.Name.ToLower()))

into:

q.FilterByString(o => o.Name, filterObject.NameOperator, filterObject.Name)

to allow to set filtering operator (i.e. EndsWith, Contains).

I've seen on google some solutions to case sensitive filtering that requires using name of the property as a string instead of stron typed propertySelectorExpression. I've also seen solution for insensitive Contains() implementation using IndexOf() bu non of them seems to fit my needs.

For now I have this but it is not working (excpetiopn on "ToLower()" call):

static MethodInfo miTL = typeof(String).GetMethod("ToLower", System.Type.EmptyTypes);
static MethodInfo miS = typeof(String).GetMethod("StartsWith", new Type[] { typeof(String) });
static MethodInfo miC = typeof(String).GetMethod("Contains", new Type[] { typeof(String) });
static MethodInfo miE = typeof(String).GetMethod("EndsWith", new Type[] { typeof(String) });

public static IQueryable<T> FilterByString<T>(this IQueryable<T> query, 
  Expression<Func<T, string>> propertySelector, 
  StringOperator operand, 
  string value)
{
    Expression constExp = Expression.Constant(value.ToLower());

    Expression dynamicExpression = null;

    switch (operand)
    {
        case StringOperator.StartsWith:
            dynamicExpression = Expression.Call(propertySelector, miTL);
            dynamicExpression = Expression.Call(dynamicExpression, miS, constExp);
            break;
        case StringOperator.Contains:
            dynamicExpression = Expression.Call(propertySelector, miTL);
            dynamicExpression = Expression.Call(dynamicExpression, miC, constExp);
            break;
        case StringOperator.EndsWith:
            dynamicExpression = Expression.Call(dynamicExpression, miTL);
            dynamicExpression = Expression.Call(dynamicExpression, miE, constExp);
            break;
        default:
            break;
    }

    LambdaExpression pred = Expression.Lambda(dynamicExpression);

    return (IQueryable<T>)query.Provider.CreateQuery(Expression.Call(typeof(Queryable), 
        "Where", new Type[] {query.ElementType}, query.Expression, pred));
}

Any ideas?


Solution

  • This should work, even if certainly not complete (nor elegant).

    public static class LinqQueries
    {
        private static MethodInfo miTL = typeof(String).GetMethod("ToLower", Type.EmptyTypes);
        private static MethodInfo miS = typeof(String).GetMethod("StartsWith", new Type[] { typeof(String) });
        private static MethodInfo miC = typeof(String).GetMethod("Contains", new Type[] { typeof(String) });
        private static MethodInfo miE = typeof(String).GetMethod("EndsWith", new Type[] { typeof(String) });
    
        public static IQueryable<T> FilterByString<T>(this IQueryable<T> query,
                                                      Expression<Func<T, string>> propertySelector,
                                                      StringOperator operand,
                                                      string value)
        {
            ParameterExpression parameterExpression = null;
            var memberExpression = GetMemberExpression(propertySelector.Body, out parameterExpression);
            var dynamicExpression = Expression.Call(memberExpression, miTL);
            Expression constExp = Expression.Constant(value.ToLower());
            switch (operand)
            {
                case StringOperator.StartsWith:
                    dynamicExpression = Expression.Call(dynamicExpression, miS, constExp);
                    break;
                case StringOperator.Contains:
                    dynamicExpression = Expression.Call(dynamicExpression, miC, constExp);
                    break;
                case StringOperator.EndsWith:
                    dynamicExpression = Expression.Call(dynamicExpression, miE, constExp);
                    break;
            }
    
            var pred = Expression.Lambda<Func<T, bool>>(dynamicExpression, new[] { parameterExpression });
            return query.Where(pred);
    
        }
    
    
        private static Expression GetMemberExpression(Expression expression, out ParameterExpression parameterExpression)
        {
            parameterExpression = null;
            if (expression is MemberExpression)
            {
                var memberExpression = expression as MemberExpression;
                while (!(memberExpression.Expression is ParameterExpression))
                    memberExpression = memberExpression.Expression as MemberExpression;
                parameterExpression = memberExpression.Expression as ParameterExpression;
                return expression as MemberExpression;
            }
            if (expression is MethodCallExpression)
            {
                var methodCallExpression = expression as MethodCallExpression;
                parameterExpression = methodCallExpression.Object as ParameterExpression;
                return methodCallExpression;
            }
            return null;
        }
    
    }
    

    Will manage

    xxx.FilterByString(m => m.Name,...)
    xxx.FilterByString(m => m.Test.Name,...)
    xxx.FilterByString(m => m.GetValue(),...)//GetValue() returns "string"
    

    CAUTION : NullReferenceExceptions aren't managed (if Name is null, or Test is null, or Test.Name is null, or GetValue() returns null)...