Search code examples
c#.netlinqlambdalinq-expressions

How to group by multiple generic linq expressions


I'm trying to use Linq expressions to construct a query, and am stuck trying to group by multiple columns. Say I have a basic collection:

IEnumerable<Row> collection = new Row[]
{
    new Row() { Col1 = "a", Col2="x" },
    new Row() { Col1 = "a", Col2="x" },
    new Row() { Col1 = "a", Col2="y" },
};

I know you can group these using lambda expressions:

foreach (var grp in collection.GroupBy(item => new { item.Col1, item.Col2 }))
{
    Debug.Write("Grouping by " + grp.Key.Col1 + " and " + grp.Key.Col2 + ": ");
    Debug.WriteLine(grp.Count() + " rows");
}

This groups correctly as you can see:

Grouping by a and x: 2 rows
Grouping by a and y: 1 rows

But now, say I receive a collection of selectors to group against, that is passed to me as a parameter in my method, and that the entity type is generic:

void doLinq<T>(params Expression<Func<T,object>>[] selectors)
{
    // linq stuff
}

Whoever's invoking the method would call like this:

doLinq<Row>(entity=>entity.Col1, entity=>entity.Col2);

How would I construct the group-by expression?

foreach (var grp in collection.GroupBy(
      item => new { 
          // selectors??
      }))
{
    // grp.Key. ??
}

Edit

I updated above to hopefully clarify why I need the set of selectors.

Edit #2

Made the entity type in doLinq generic.


Solution

  • The solution worked for me. It involves two parts:

    • create a grouping object (which I implemented inelegantly as object[]) given the row value and the set of selectors. This involves a lambda expression that compiles and invokes each selector on the row item.
    • implement IEquality for the grouping object type (in my case that's IEqualityComparer).

    First part

    foreach (System.Linq.IGrouping<object[], T> g in collection.GroupBy(
        new Func<T, object[]>(
            item => selectors.Select(sel => sel.Compile().Invoke(item)).ToArray()
        ),
        new ColumnComparer()
    )
    { ... }
    

    Second Part

    public class ColumnComparer : IEqualityComparer<object[]>
    {
        public bool Equals(object[] x, object[] y)
        {
            return Enumerable.SequenceEqual(x, y);
        }
    
        public int GetHashCode(object[] obj)
        {
            return (string.Join("", obj.ToArray())).GetHashCode();
        }
    }
    

    This works for basic Linq, and Linq for the MySql connector. Which other Linq providers, and which expression types this works for is a whole other question ...