2

After a good dose of Googling and trying some things and not finding/getting the desired result I decided to post this question.

I have a custom made OrderBy extension method and now when performing the OrderBy operation I'd like to pass an AlphanumComparator like this:

return divergences.OrderBy(sort, new AlphanumComparator());

Here's the extension method:

public static IQueryable<T> OrderBy<T>(this IQueryable<T> collection,
    GridSortOptions sortOptions, AlphanumComparator comparer = null)
{
    if (string.IsNullOrEmpty(sortOptions.Column))
    {
        return collection;
    }

    Type collectionType = typeof(T);

    ParameterExpression parameterExpression = Expression.Parameter(collectionType, "p");

    Expression seedExpression = parameterExpression;

    Expression aggregateExpression = sortOptions.Column.Split('.').Aggregate(seedExpression, Expression.Property);

    MemberExpression memberExpression = aggregateExpression as MemberExpression;

    if (memberExpression == null)
    {
        throw new NullReferenceException(string.Format("Unable to cast Member Expression for given path: {0}.", sortOptions.Column));
    }

    LambdaExpression orderByExp = Expression.Lambda(memberExpression, parameterExpression);

    const string orderBy = "OrderBy";

    const string orderByDesc = "OrderByDescending";

    Type childPropertyType = ((PropertyInfo)(memberExpression.Member)).PropertyType;

    string methodToInvoke = sortOptions.Direction == MvcContrib.Sorting.SortDirection.Ascending ? orderBy : orderByDesc;

    MethodCallExpression orderByCall;

    orderByCall = Expression.Call(typeof(Queryable), methodToInvoke, new[] { collectionType, childPropertyType }, collection.Expression, Expression.Quote(orderByExp));

    if(comparer != null)
    {
       // How can I pass the comparator to the OrderBy MethodCallExpression?

       // Using the standard LINQ OrderBy, we can do this:
       // elements.OrderBy(e => e.Index, new AlphanumComparator())
    }

    return collection.Provider.CreateQuery<T>(orderByCall);
}

See the comment in the code where I think I should pass the IComparer... how could I approach this?

Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
  • Fundamentally you've got a problem here - you're expecting an *arbitrary comparer* to be turned into SQL. How do you expect that to work? If you implemented `IComparer` in your own code and made it order by hash code, what would you expect the generated SQL to look like? – Jon Skeet Jul 22 '13 at 21:08
  • @JonSkeet what if I declare the parameter to be `AlphanumComparator` instead of the arbitrary `IComparer`? I'm only passing the comparer for specific properties that I know are of type string. – Leniel Maccaferri Jul 22 '13 at 21:13
  • Assuming that's your own type, it has the same problem: nothing in the LINQ provider will know what to do with it. – Jon Skeet Jul 22 '13 at 21:15
  • @JonSkeet OK. I think I got it... the comparison is done in memory... not on SQL Server so there's no logic in doing it the way I want. Learning and learning and learning... :) Now, how could i pass the comparer after getting the query? That's the question... – Leniel Maccaferri Jul 22 '13 at 21:26
  • I'm not sure what you mean by "getting the query" - you could use `AsEnumerable()` to effectively make the rest of the query execute in memory... – Jon Skeet Jul 23 '13 at 05:26
  • Hey @JonSkeet: take a look at my answer... Thanks for the comments! :) – Leniel Maccaferri Aug 01 '13 at 02:41

1 Answers1

0

I had to approach this differently.

I was trying to create a generic OrderBy to be used with MvcContrib Grid, but passing the IComparer to that custom OrderBy expression did not work as I imagined it would work.

So I created this helper that receives a string in dot notation like Element1.Standard.Chapter.Manual.Name and then returns an Expression<Func<T, string>>:

public static Func<T, string> CreateSelectorExpression<T>(string propertyName) where T : class
{
    ParameterExpression parameterExpression = Expression.Parameter(typeof(T));

    Expression aggregateExpression = propertyName.Split('.').
        Aggregate(parameterExpression as Expression, Expression.Property) as MemberExpression;

    LambdaExpression exp = Expression.Lambda(aggregateExpression, parameterExpression);

    return (Func<T, string>)exp.Compile();
}

This expression typed to T (in this case Divergence object type) can then be passed (see func.Invoke) to the standard LINQ OrderBy operator where I can also pass the custom IComparer AlphanumComparator like this:

if (sort.Column.Contains("Index"))
{
    var func = Helpers.ExtensionMethods.CreateSelectorExpression<Divergence>(sort.Column);

    if (sort.Direction == SortDirection.Ascending)
    {
        return divergences.OrderBy(func, new AlphanumComparator());
    }
    else
    {
        return divergences.OrderByDescending(func, new AlphanumComparator());
    }
}

This involved a little bit more work but solved the problem in a generic fashion the way I wanted it to be.

Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
  • You realize that now this ordering *doesn't* happen in the database, and anything else later in the query will also be performed locally too, right? – Jon Skeet Aug 01 '13 at 05:56
  • Sure @JonSkeet... that's the only way I found to make it work the way I wanted since the custom `IComparer` can only be performed locally and not on the database side as you mentioned. Do you see any way to improve this code? – Leniel Maccaferri Aug 01 '13 at 06:19
  • Well, it's not clear why you need to create an `Expression` at all now - you just need a delegate. I suggest you change `CreateSelectorExpression` to return `Func`, just fetch the property with reflection and call `Delegate.CreateDelegate`. – Jon Skeet Aug 01 '13 at 06:22
  • @JonSkeet I changed `CreateSelectorExpression` to return a `Func` delegate instead. – Leniel Maccaferri Aug 01 '13 at 06:45