6

I am using below code for Generic Filter, any search text passed but the contains method is Case sensitive, how can I write to ignore case.

public static class QueryExtensions
{
    public static IQueryable<T> Filter<T>(this IQueryable<T> query, string search)    
    {           
        var properties = typeof(T).GetProperties().Where(p => 
                /*p.GetCustomAttributes(typeof(System.Data.Objects.DataClasses.EdmScalarPropertyAttribute),true).Any() && */
                p.PropertyType == typeof(String));        

        var predicate = PredicateBuilder.False<T>();
        foreach (var property in properties )
        {
           predicate = predicate.Or(CreateLike<T>(property,search));
        }
        return query.AsExpandable().Where(predicate);
    }
    private static Expression<Func<T,bool>> CreateLike<T>( PropertyInfo prop, string value)
    {       
        var parameter = Expression.Parameter(typeof(T), "f");
        var propertyAccess = Expression.MakeMemberAccess(parameter, prop);                    
        var like = Expression.Call(propertyAccess, "Contains", null, Expression.Constant(value,typeof(string)));

        return Expression.Lambda<Func<T, bool>>(like, parameter);       
    }

}
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
ineffable p
  • 1,207
  • 2
  • 22
  • 46
  • Have you tried converting both the search string and the string that you are trying to search to a particular case before comparing something like `if(str1.ToLower() == str2.ToLower())` – Vishweshwar Kapse Jul 25 '13 at 10:11

4 Answers4

12

Instead of calling String.Contains, call String.IndexOf with a case insensitive StringComparison parameter. Then compare its result with 0, with the Expression.GreaterThanOrEqual expression. You need to provide the extra parameter in your Expression.Call as an Expression.Constant.

You can decide to hardcode one of the case-insensitive StringComparison options, or export it as a parameter of the Filter method, allowing users to decide whether they want case-insensitive search or not.

You can do something like this:

    private static Expression<Func<T, bool>> CreateLike<T>(PropertyInfo prop, string value)
    {
        var parameter = Expression.Parameter(typeof(T), "f");
        var propertyAccess = Expression.MakeMemberAccess(parameter, prop);

        var indexOf = Expression.Call(propertyAccess, "IndexOf", null, Expression.Constant(value, typeof(string)),Expression.Constant(StringComparison.InvariantCultureIgnoreCase));
        var like=Expression.GreaterThanOrEqual(indexOf, Expression.Constant(0));
        return Expression.Lambda<Func<T, bool>>(like, parameter);
    }

or, with the StringComparison parameter

    private static Expression<Func<T, bool>> CreateLike<T>(PropertyInfo prop, 
        string value, 
        StringComparison comparison=StringComparison.InvariantCultureIgnoreCase)
    {
        var parameter = Expression.Parameter(typeof(T), "f");
        var propertyAccess = Expression.MakeMemberAccess(parameter, prop);

        var indexOf = Expression.Call(propertyAccess, "IndexOf", null, 
            Expression.Constant(value, typeof(string)),
            Expression.Constant(comparison));
        var like=Expression.GreaterThanOrEqual(indexOf, Expression.Constant(0));
        return Expression.Lambda<Func<T, bool>>(like, parameter);
    }

By using a default value for comparison you avoid creating two overloads for the same job.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Awesome! I've been struggling with this all morning and you serve it up to me on a silver plate. :) – erroric Mar 25 '15 at 16:35
1

You could try using String.IndexOf instead.

string x,y = string.Empty;
x.IndexOf(y,0,x.Length, StringComparison.CurrentCultureIgnoreCase) > -1

As it has a StringComparison parameter.

This would return an integer

var like = Expression.Call(propertyAccess, "IndexOf", null, Expression.Constant(value, typeof(string)), Expression.Constant(StringComparison.CurrentCultureIgnoreCase,typeof(StringComparison)));
ywm
  • 1,107
  • 10
  • 14
  • but how can I this line in Expression.Call Expression.Call(expression, "Contains", null, Expression.Constant(filterText, typeof (string))); – ineffable p Jul 25 '13 at 10:28
  • @ineffablep You could wrap the like expression inside another func that does a `>` comparison. I've not done much in the way of creating Generic filtering. If you can wait 9 hours I could put a better answer when I get home from work ;) – ywm Jul 25 '13 at 10:42
1

Please refer to the following code if you want to filter or search for a value from the list. In addition, it is a generic method that will help you filter any type of class or object from the list. It is working as a like clause in SQL such as (column1 like '%abc%' or column2 like '%abc%').

public static class Filter<T>
{
    public static Expression<Func<T, bool>> FilterExpression(string searchValue)
    {
        Expression finalExpression = Expression.Constant(false);
        var parameter = Expression.Parameter(typeof(T), "x");
        PropertyInfo[] propertyInfos = typeof(T).GetProperties();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                if (propertyInfo.PropertyType == typeof(string))
                {
                    var propertyExpn = Expression.Property(parameter, propertyInfo.Name.Trim().ToLower());
                    var containsExpn = Expression.Call(propertyExpn, "Contains", null, Expression.Constant(searchValue, typeof(string)), Expression.Constant(StringComparison.InvariantCultureIgnoreCase));
                    var nullCheckExpn = Expression.NotEqual(propertyExpn, Expression.Constant(null, typeof(string)));
                    var andAlsoExpn = Expression.AndAlso(nullCheckExpn, containsExpn);
                    finalExpression = Expression.Or(finalExpression, andAlsoExpn);
                }
            }
            var rowFilterLambdaExpression = Expression.Lambda<Func<T, bool>>(finalExpression, new ParameterExpression[] { parameter });
            return rowFilterLambdaExpression;
    }
}

Usage E.g.,

var result = 
 dataItems.Where(Filter<T>.FilterExpression(model.FilterValue).Compile()).ToList();
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
-1

It's probably simplest to convert both parameters to upper case first (upper case conversions are optimized better than lower case ones in .NET). You can then do your comparison.

Upper case conversion can be done like this:

var expression = Expression.Call(property, typeof(string).GetMethod("ToUpperInvariant", System.Type.EmptyTypes));
pattermeister
  • 3,132
  • 2
  • 25
  • 27
  • Better yet, don't do any conversion at all. Use a comparison that doesn't take casing into account. This way you avoid creating throwaway strings and hurting performance by creating and comparing two more strings per item. Not to mention, avoid casing quirks in various cultures – Panagiotis Kanavos Jul 25 '13 at 10:36
  • I don't think it is possible to do a case insensitive comparison that "doesn't take casing into account". How can you compare "Dog" and "dog" without considering casing (and, as you say, culture rules)? But your point about needless string allocation is a good one for a high performance situation, thanks - no need to give the GC extra work. – pattermeister Jul 25 '13 at 10:44
  • You let the framework handle it. It already implements this, in all functions that accept a StringComparison parameter. In fact, the call eventually gets routed to the appropriate CultureInfo object, which obviously knows these rules. ToUpper() also uses the CultureInfo to return the proper uppercase form as well. – Panagiotis Kanavos Jul 25 '13 at 10:58