Case insensitive string comparisons with LINQ Dynamic Query

LINQ rocks. It really does.

One down-side to LINQ is that, out of the box, it’s geared towards knowing your query structure at compile-time. The values can be dynamic, of course, but it’s assumed that the structure of your query is static. For example, if you want to select a set of "Person" objects from the "People" collection where Person.FirstName starts with "Aar", you could write it as such:

var results = from person in People
              where person.FirstName.StartsWith("Aar")
              select person;

That’s all fine and good, but what about scenarios where you want to dynamically build up your query structure? In our client application we have address books (directories) that include the ability to filter them on any, or nearly any, column:

How would I accomplish this with LINQ? Not easily. Just ask Ayende or Rob Conery, both of whom have blogged about some of their adventures in advanced usage scenarios. Enter the LINQ Dynamic Query sample from Microsoft. As usual, ScottGu’s got a good write-up. In a nutshell, it’s a custom expression tree generator based on a limited (but useful) string-based query grammar. With Dynamic Query I could write the query above like this:

var results = from person in People
              select person;
results = results.Where("FirstName.StartsWith(\"Aar\")");

It solved my problem nicely. Almost. As with my example above about matching FirstName’s, let me ask: how often does a user enter an exact case-sensitive match for what they’re looking for? I can save you the trouble and tell you: it doesn’t matter. It’s an unacceptable requirement for a user to have to match something exactly. It’s already questionable that we don’t automatically use fuzzy matching algorithms.

So what I really want is to specify a StringComparison enum value on the call to "StartsWith":

var results = from person in People
              select person;
results = results.Where("FirstName.StartsWith(\"Aar\", System.StringComparison.OrdinalIgnoreCase)");

Alas, this breaks. LINQ Dynamic Query doesn’t support enum values as parameters to methods. So I added it. I won’t redistribute the sample (I’m pretty sure I can’t, but I don’t care to anyway) so here’s what you need to do to add support for enum parsing. Note that I’ve only tested it with calls to string’s StartsWith(string, StringComparison) method. I don’t know what will happen if you sprinkle enum values in random places throughout your dynamic query. Work on My Machine, your mileage may vary, etc. etc. etc.

1. Download the sample.

2. Crack open the Dynamic.cs source file. It’s scary, but you can do it. Modify it like so (I added the "if (ParseEnumType…"

Expression ParseIdentifier() {
    ValidateToken(TokenId.Identifier);
    object value;
    if (keywords.TryGetValue(token.text, out value)) {
        if (value is Type) return ParseTypeAccess((Type)value);
        if (value == (object)keywordIt) return ParseIt();
        if (value == (object)keywordIif) return ParseIif();
        if (value == (object)keywordNew) return ParseNew();
        NextToken();
        return (Expression)value;
    }
    if (symbols.TryGetValue(token.text, out value) ||
        externals != null && externals.TryGetValue(token.text, out value)) {
        Expression expr = value as Expression;
        if (expr == null) {
            expr = Expression.Constant(value);
        }
        else {
            LambdaExpression lambda = expr as LambdaExpression;
            if (lambda != null) return ParseLambdaInvocation(lambda);
        }
        NextToken();
        return expr;
    }
    // ADD THIS IF STATEMENT
    if (ParseEnumType(out value))
    {
        Expression expr = Expression.Constant(value);
        NextToken();
        return expr;
    }
    if (it != null) return ParseMemberAccess(null, it);
    throw ParseError(Res.UnknownIdentifier, token.text);
}

3. Add the definition for ParseEnumType. This little bit of nastiness is essentially doing a look-ahead to resolve a type name, since most of the parser’s rules are built to process more contextual information (such as a property name of a type, etc.) In our case, we need to attempt to match "Foo.Foo.Foo" to a type name, and if it doesn’t end up resolving, we need to reset the parser back to the beginning of "Foo" to continue parsing.

bool ParseEnumType(out object value)
{
    value = null;

    ValidateToken(TokenId.Identifier);
    Type enumType = null;
    int position = token.pos;
    string typeName = token.text;
    while (enumType == null)
    {
        // Loop until we stop processing identifiers and/or dots
        enumType = Type.GetType(typeName, false, true);
        if (enumType == null)
        {
            NextToken();
            if (token.id == TokenId.Dot)
            {
                typeName += token.text;
                NextToken();
                if (token.id == TokenId.Identifier)
                {
                    typeName += token.text;
                }
                else
                {
                    break;
                }
            }
            else
            {
                break;
            }
        }
    }

    if ((enumType != null) && IsEnumType(enumType))
    {
        NextToken();
        ValidateToken(TokenId.Dot, Res.DotExpected);
        NextToken();
        ValidateToken(TokenId.Identifier, Res.IdentifierExpected);
        value = Enum.Parse(enumType, token.text, true);
        return true;
    }
    else
    {
        SetTextPos(position);
        NextToken();
    }

    return false;
}

4. Add an error "resource" string (but not really a true resource string) to the "Res" static class. We added a new condition, so we need an error message to match.

public const string DotExpected = "'.' expected";

Voila! Make sure your enum values are fully-qualified type names and you’ll be good to go.

Hopefully this works for you as well as it did for me, and I have to say I can’t believe I couldn’t find this on the ‘net, as I imagine this is a very common use-case.

This entry was posted on Monday, December 15th, 2008 at 2:46 am and is filed under .net, tips and tricks. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

6 Responses to “Case insensitive string comparisons with LINQ Dynamic Query”

  1. Beau Crawford Says:

    Exactly what I was looking for. Worked like a charm. Thanks much.

  2. Nasir Says:

    Exactly what I needed. Thank you very much!

  3. Dilyan Rusev Says:

    There is a better approach. On comparson, remember the left expression type if it is enum.

    In parse enum, simply try to resolve that type, and set it to null if you succeed.

    Then you would have sth like “Status = Published” if Status is of type CommentStatus, defined as:

    public enum CommentStatus { Published, Invisible }

  4. Alexei Belakurski Says:

    There is a slight problem when enum you want to use is not in current assembly or mscorlib.dll, Type.GetType require fully qualified assembly name in this case. (e.g. WPF enums like System.Windows.Visibility). I guess one possible solution is to provide DynamicExpression with list of assemblys to look for.

  5. Kevin Hoffman Says:

    I tried this code because right now I get only case sensitive comparisons in my dynamic SQL queries over an in-memory object model. When I tried this code, it threw an exception in the Dynamic.cs file complaining that “System.String” did not contain a method called “Contains”

  6. Aaron Fischer Says:

    I think you could have just done
    results.Where(“FirstName.StartsWith(\”Aar\”, @0)”, System.StringComparison.OrdinalIgnoreCase );