1

I want to create a dynamic GroupBy implementation that ignores case. I am using Expression.Call, which allows me to pass Expressions as arguments.

There are several answers on how to create a custom comparer, but this question is about how to pass a comparer dynamically.

Here is the complete method:

public static IQueryable GroupBy(this IQueryable source, string keySelector, string elementSelector, params object[] values)
{
    if (source == null) throw new ArgumentNullException("source");
    if (keySelector == null) throw new ArgumentNullException("keySelector");
    if (elementSelector == null) throw new ArgumentNullException("elementSelector");
    LambdaExpression keyLambda = DynamicExpression.ParseLambda(source.ElementType, null, keySelector, false, values);
    LambdaExpression elementLambda = DynamicExpression.ParseLambda(source.ElementType, null, elementSelector, false, values);

    return source.Provider.CreateQuery(
        Expression.Call(
            typeof(Queryable), 
            "GroupBy", 
            new Type[] { source.ElementType, keyLambda.Body.Type, elementLambda.Body.Type },
            source.Expression, 
            Expression.Quote(keyLambda), 
            Expression.Quote(elementLambda)
        )
    );
}

The call to Queryable.GroupBy is created by:

Expression.Call(typeof(Queryable), "GroupBy", 
  new Type[] { source.ElementType, keyLambda.Body.Type, elementLambda.Body.Type },
  source.Expression, Expression.Quote(keyLambda), Expression.Quote(elementLambda))

Queryable.GroupBy allows to pass a custom IEqualityComparer. How can I do this? Expression.Call only allows me to pass arguments of type Expression.

Is there any other way I can group with case ignored, by e.g. dynamically overriding GetHashCode() of the keys?

Jaroslav K
  • 327
  • 1
  • 13
  • Possible duplicate of [writing a custom comparer for linq groupby](https://stackoverflow.com/questions/37733773/writing-a-custom-comparer-for-linq-groupby) – Feras Al Sous Sep 25 '19 at 14:21
  • @FerasAlSous The issue here is that I am creating a call to GroupBy dynamically. This is not about implementing the comparer. – Jaroslav K Sep 25 '19 at 14:25

2 Answers2

1

You should call it the same way, as you would normally by adding comparer in GroupBy call:

return source.Provider.CreateQuery(
    Expression.Call(
        typeof(Queryable),
        "GroupBy", 
        new Type[] { source.ElementType, keyLambda.Body.Type, elementLambda.Body.Type },
        source.Expression,
        Expression.Quote(keyLambda),
        Expression.Quote(elementLambda),
        Expression.Constant(StringComparer.InvariantCultureIgnoreCase)
    )
);
Krzysztof
  • 15,900
  • 2
  • 46
  • 76
  • Thank you for the answer. When I do that, I get an exception `No generic method 'GroupBy' on type 'System.Linq.Queryable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic.` – Jaroslav K Sep 26 '19 at 07:59
  • You need to provide more code if you want some help on that. For my test case this code worked. – Krzysztof Sep 26 '19 at 08:28
  • I think the issue here is that I have a type that is determined at run-time, while `StringComparer` only works with `string`. The type in `keyLambda.Body.Type` at run-time is something like `DynamicClass2` – Jaroslav K Sep 27 '19 at 12:14
  • It might be it, but ignore case grouping make sense only for strings if you think about it, so maybe you try to add condition and pass comparer for strings only. – Krzysztof Sep 27 '19 at 13:50
  • I am using LINQ to SQL. The strings are in the dynamic class. What I can do is `var comparerType = typeof(DynamicComparer<>).MakeGenericType(keyLambda.Body.Type); var keyComparer = Activator.CreateInstance(comparerType);` and then `Expression.Constant(keyComparer)` – Jaroslav K Sep 27 '19 at 14:50
  • Then I have to come up with a comparer that resolves the dynamic class and compares strings with `StringComparer.InvariantCultureIgnoreCase`. But only if the query is done locally (because SQL is case insensitive). – Jaroslav K Sep 27 '19 at 14:56
0

StringComparer can not be used here because the type is dynamic and not string. I had to elaborate on Krzysztofs answer to find a solution that worked.

First create an instance of a custom dynamic comparer DynamicCaseInsensitiveComparer<T> (which implements IEqualityComparer) of the same type as keyLambda.Body.Type. Since the type is provided by a variable, you have to use MakeGenericType. Then add it in the GroupBy call:

var comparerType = typeof(DynamicCaseInsensitiveComparer<>).MakeGenericType(keyLambda.Body.Type);
var keyComparer = Activator.CreateInstance(comparerType);

return source.Provider.CreateQuery(
    Expression.Call(
        typeof(Queryable),
        "GroupBy", 
        new Type[] { source.ElementType, keyLambda.Body.Type, elementLambda.Body.Type },
        source.Expression,
        Expression.Quote(keyLambda),
        Expression.Quote(elementLambda),
        Expression.Constant(keyComparer)
    )
);

How to create a custom comparer has been answered in other questions, see for example IEqualityComparer for Annoymous Type

Jaroslav K
  • 327
  • 1
  • 13