Case insensitive string compare in LINQ expression
Asked Answered
A

2

7

I'm trying to write an ExpressionVisitor to wrap around my LINQ-to-object expressions to automatically make their string comparisons case insensitive, just as they would be in LINQ-to-entities.

EDIT: I DEFINITELY want to use an ExpressionVisitor rather than just applying some custom extension or something to my expression when it is created for one important reason: The expression being passed to my ExpressionVisitor is generated by the ASP.Net Web API ODATA layer, so I don't have control over how it is generated (i.e. I can't lowercase the string it is searching for except from within this ExpressionVisitor).

Has to support LINQ to Entities. Not just extension.

Here's what I have so far. It looks for a call to "Contains" on a string and then calls ToLower on any member access inside that expression.

However, it's not working. If I view the expressions after my changes, it looks correct to me, so I'm not sure what I could be doing wrong.

public class CaseInsensitiveExpressionVisitor : ExpressionVisitor
{

    protected override Expression VisitMember(MemberExpression node)
    {
        if (insideContains)
        {
            if (node.Type == typeof (String))
            {
                var methodInfo = typeof (String).GetMethod("ToLower", new Type[] {});
                var expression = Expression.Call(node, methodInfo);
                return expression;
            }
        }
        return base.VisitMember(node);
    }

    private Boolean insideContains = false;
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.Name == "Contains")
        {
            if (insideContains) throw new NotSupportedException();
            insideContains = true;
            var result = base.VisitMethodCall(node);
            insideContains = false;
            return result;
        }
        return base.VisitMethodCall(node);
    }

If I set a breakpoint on the "return expression" line in the VisitMember method and then do a "ToString" on the "node" and "expression" variables, the break point gets hit twice, and here's what the two sets of values are:

First hit:

node.ToString()
"$it.LastName"
expression.ToString()
"$it.LastName.ToLower()"

Second hit:

node.ToString()
"value(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty"
expression.ToString()
"value(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty.ToLower()"

I don't know enough about expressions to figure out what I'm doing wrong at this point. Any ideas?

Antibaryon answered 2/7, 2013 at 14:34 Comment(5)
string.Equals(string1, string2, StringComparison.InvariantCultureIgnoreCase)?Cling
Avoid ToLower for string comparison as it's more likely to result in an error (Turkey Test). Either use Uppercase or preferably, as Corak suggested, String.Equals.Vesicatory
This won't work in my case. First, I don't have control over the Expression, since it's automatically generated by the ASP.Net Web API. Second, I want something that I can generically use to wrap a LINQ statement and will work with both LINQ-to-entities and LINQ-to-objects.Antibaryon
@keyboardP: Yeah, I read about the Turkey Test. I'm not concerned with that at this point. But I'll try using Uppercase, instead, once I get this working.Antibaryon
@JoshMouch, Trust you're doing well, Did you end up writing entire ODATA --> LINQ --> SQL manually, no ODATA V4 framework didn't help? https://mcmap.net/q/507293/-odata-case-in-sensitive-filtering-in-web-api/1431250Shag
E
3

I made a sample app from your code and it seems working:

    public class Test
{
    public string Name;
}
public class CaseInsensitiveExpressionVisitor : ExpressionVisitor
{

    protected override Expression VisitMember(MemberExpression node)
    {
        if (insideContains)
        {
            if (node.Type == typeof (String))
            {
                var methodInfo = typeof (String).GetMethod("ToLower", new Type[] {});
                var expression = Expression.Call(node, methodInfo);
                return expression;
            }
        }
        return base.VisitMember(node);
    }

    private Boolean insideContains = false;

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.Name == "Contains")
        {
            if (insideContains) throw new NotSupportedException();
            insideContains = true;
            var result = base.VisitMethodCall(node);
            insideContains = false;
            return result;
        }
        return base.VisitMethodCall(node);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Expression <Func<Test, bool>> expr = (t) => t.Name.Contains("a");
        var  expr1 = (Expression<Func<Test, bool>>) new CaseInsensitiveExpressionVisitor().Visit(expr);
        var test = new[] {new Test {Name = "A"}};
        var length = test.Where(expr1.Compile()).ToArray().Length;
        Debug.Assert(length == 1);
        Debug.Assert(test.Where(expr.Compile()).ToArray().Length == 0);

    }
}
Edithe answered 2/7, 2013 at 16:19 Comment(6)
Hmm.... could it have something to do with the really long expression being passed into contains (noted at the end of my question): "value(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty". I don't know what it is, but I still apply ToLower to it, since it's a String type.Antibaryon
Or could it have something to do with the fact that LINQ to Entities is executing the Expression? If I try to compile/invoke the expression directly, I get a message about "This method supports the INQ to Entities infrastructure and is not intended to be used directly from your code).Antibaryon
Sorry, I am not familiar with OData.Edithe
You said that you want linq2Objects have same behavior. So I assume that you had made query against object. If error "This method supports the.." appears usually it means that you lost context with db. for example, invoke expression after context disposed.Edithe
Actually, you are right. My code is working! The problem was that the IQueryable was converted to an IList and then back to an IQueryable before passed back from the OData Get. The first IQueryable had my ExpressionVisitor applied, but not the second. Marking yours as the answer since you got me thinking.Antibaryon
@Ben, May I request your help pls. https://mcmap.net/q/507293/-odata-case-in-sensitive-filtering-in-web-api/1431250Shag
L
0

you can create a extesion method like this:

public static class Extensions
{
    public static bool InsensitiveEqual(this string val1, string val2)
    {
        return val1.Equals(val2, StringComparison.OrdinalIgnoreCase);
    }
}

And then you can call like this:

string teste = "teste";
string teste2 = "TESTE";

bool NOTREAL = teste.Equals(teste2); //FALSE
bool REAL = teste.InsensitiveEqual(teste2); //true
Limacine answered 2/7, 2013 at 14:51 Comment(2)
Thanks, but that won't work. I want to use an ExpressionVisitor so I can wrap my LINQ statement such that I can use it both in LINQ-to-objects and LINQ-to-Entities. A custom extension InsensitiveEqual() will not work in LINQ-to-Entities.Antibaryon
Additionally, I don't have control over the creation of the expression, since the ASP.Net Web API Odata layer is generating the expression.Antibaryon

© 2022 - 2024 — McMap. All rights reserved.