LINQ OrderBy anonymous object with projection comparer
Asked Answered
T

3

8

Ive been trying to get OrderBy in a LINQ statement to work with an anonymous object but have failed by now.

I checked these already:
Anonymous IComparer implementation
C# linq sort - quick way of instantiating IComparer
How to sort an array of object by a specific field in C#?

I spent a few hours trying different approaches but there has to be something I'm missing.

Let's say there's the following class:

public class Product
{
   public int Id {get; set;}
   public string Name {get; set;}
   public int Popularity {get; set;}
   public decimal Price {get; set;}
}

And products is a list of these objects.

How can I complete this LINQ statement, so that it works with the anonymous object ?
To be clear, I know I can do this in a different way but I'd be very interested to learn how to make this particular example work.

var sortedProducts = products
                       .OrderBy(p => 
                              new {p.Popularity, p.Price}, 
                              [IComparer magic goes here]);

It seems that it should be possible with an implementation of the ProjectionComparer:
http://code.google.com/p/edulinq/source/browse/src/Edulinq/ProjectionComparer.cs?r=0c583631b709679831c99df2646fc9adb781b2be

Any ideas how to do this ?

UPDATE:

I did a quick performance test on this - the anonymous comparer solution vs standard orderby.thenby and it seems that the anonymous solution is quite slower which is probably what we might have expected anyway.

         numProd  | Anon    | chained orderby clauses
         10 000   | 47 ms   | 31 ms
         100 000  | 468 ms  | 234 ms
         1 000 000| 5818 ms | 2387 ms
         5 000 000| 29547 ms| 12105 ms
Tillio answered 27/4, 2012 at 19:42 Comment(5)
what's your ordering criteria? Are you saying it should be by Popularity first, then by price? Why not do OrderBy on popularity and then ThenBy on price?Sampson
@JamesMichaelHare - let's say higher popularity as a primary factor, lower price as a secondary factor if that makes any difference for the example. EDIT: I know I can do this but I wonder if the approach with the anonymous object will work at all, and if so, I just can't figure out how.Tillio
Why use an anonymous object at all in the OrderBy() then? Why not just: products.OrderByDescending(p => p.Popularity).ThenBy(p => p.Price)Sampson
And i know you indicated that you know there are other ways of doing this, I'm afraid I'm just curious why the anonymous type in an OrderBy()?Sampson
@JamesMichaelHare - no reason other than I've read that it can be done but I don't know how. It'd be great to learn how to do this, even if it's not something I intend to use every day as usually with such weird examples you get to know things that are useful in other cases.Tillio
E
8

You can make an IComparer<T> implementation that uses a delegate that you supply for the comparison, and instantiate it with type inference (similar to "cast by example"):

static class AnonymousComparer
{
    public static IComparer<T> GetComparer<T>(T example, Comparison<T> comparison)
    {
        return new ComparerImpl<T>(comparison);
    }
    private class ComparerImpl<T> : IComparer<T>
    {
        private readonly Comparison<T> _comparison;
        public ComparerImpl(Comparison<T> comparison) { _comparison = comparison; }
        public int Compare(T x, T y) { return _comparison.Invoke(x, y); }
    }
}

And use it thus:

var comparer = AnonymousComparer.GetComparer(
    new { Popularity = 0, Price = 0m },
    (a, b) => //comparison logic goes here
    );

var sortedProducts = products
    .OrderBy(p =>
        new { p.Popularity, p.Price },
        comparer); 

EDIT: I just checked out the projection comparer page you linked to. With that approach, you don't need the "example" argument for type inference. The approach still needs to be adapted, however, to take a delegate instead of an interface. Here it is:

//adapted from http://code.google.com/p/edulinq/source/browse/src/Edulinq/ProjectionComparer.cs?r=0c583631b709679831c99df2646fc9adb781b2be
static class AnonymousProjectionComparer
{
    private class ProjectionComparer<TElement, TKey> : IComparer<TElement>
    {
        private readonly Func<TElement, TKey> keySelector;
        private readonly Comparison<TKey> comparison;

        internal ProjectionComparer(Func<TElement, TKey> keySelector, Comparison<TKey> comparison)
        {
            this.keySelector = keySelector;
            this.comparison = comparison ?? Comparer<TKey>.Default.Compare;
        }

        public int Compare(TElement x, TElement y)
        {
            TKey keyX = keySelector(x);
            TKey keyY = keySelector(y);
            return comparison.Invoke(keyX, keyY);
        }
    }

    public static IComparer<TElement> GetComparer<TElement, TKey>(Func<TElement, TKey> keySelector, Comparison<TKey> comparison)
    {
        return new ProjectionComparer<TElement, TKey>(keySelector, comparison);
    }
}
Evangelinaevangeline answered 27/4, 2012 at 19:59 Comment(1)
Brilliant! I checked the first version and it works as expected. I will check the adapted ProjectionComparer as well and then learn more about cast by example. Thanks very much for your comprehensive answer!Tillio
S
5

You don't really need an anonymous object to order these objects by populartiy descending and then price, you can use OrerBy and ThenBy in combination, like:

var sortedProducts = products.OrderByDescending(p => p.Popularity)
    .ThenBy(p => p.Price);

To do an IComparer<T> on an anonymous type, you'd be best off using a factory to construct one from a delegate and using type inference (specifying anonymous types without inference is a pain!).

You might want to measure the performance implications of creating anonymous objects purely for ordering, but Phoogs answer gives a good way to use Comparison<T> delegate to construct an IComparer<T> on the fly..

Sampson answered 27/4, 2012 at 19:53 Comment(3)
see above - if it was trivial I'd hope that I would have been able to do it without help. I know the problem can be solved in a different way.Tillio
@Joanna: understood, just wanted to make sure it wasn't an issue of knowing that OrderBy/ThenBy can be chained. Phoog has a great solution below.Sampson
I updated with results of some brief performance testing - the anonymous comparer solution is slower as expected. It's good to know.Tillio
D
0

Not exactly an answer... but too long for comment: It is hard to create sensible generic comparer.

While there is well established comparison relation for objects by single property there no such thing for multiple or even 2 properties. I.e. this is very common problem when you try to order points on flat surface: just 2 values (x,y) but there is no way to say (x1,y1) < (x2,y2) so everyone agrees with it.

In most cases you end up saying order by attribute 1, than by attribute 2,... or by mapping all attributes to single value (i.e. by simply multiplying all of them). These approaches are easily expressed without need of generic comparer in LINQ:

  • ordering by attributes with chained OrderBy(attr1).OrderBy(attr2)....
  • ordering by metric OrderBy(attr1 * attr2) (or any other Metric on your objects)
Demicanton answered 27/4, 2012 at 19:54 Comment(1)
What I'm interested in is basically the implementation of this thing: code.google.com/p/edulinq/source/browse/src/Edulinq/… . I suppose if I get this right the rest would just work, but I would need help exactly with this.Tillio

© 2022 - 2024 — McMap. All rights reserved.