Handling related generic type parameters In answering a question on StackOverflow, it occurred to me that a more generic version of GroupBy for C# that delegated membership in each group to a class could be useful.

This is my attempt, but I am unsatisfied with a few things. I don't particularly like the need to constrain the element type to IOrderable - I feel like it should be possible to delegate ordering to the group type somehow (I really wanted a static method constraint in the IGGrouping interface).

public interface IOrderable&lt;T&gt; {
    double ordering();
}

public interface IGGrouping&lt;T&gt; : IEnumerable&lt;T&gt; where T : IOrderable&lt;T&gt; {
    bool BelongsToGroup(T aT);
    IGGrouping&lt;T&gt; Add(T aT);
}

public static class IEnumerableExt {
    public static IEnumerable&lt;IGGrouping&lt;T&gt;&gt; GroupBy&lt;TG,T&gt;(this IEnumerable&lt;T&gt; src) where TG : IGGrouping&lt;T&gt;, new() where T : IOrderable&lt;T&gt; {
        var groups = new List&lt;IGGrouping&lt;T&gt;&gt;();
        void Add(T aT) {
            var found = false;
            foreach (var g in groups) {
                found = g.BelongsToGroup(aT);
                if (found) {
                    g.Add(aT);
                    break;
                }
            }
            if (!found)
                groups.Add(new TG().Add(aT));
        }

        foreach (var s in src.OrderBy(s =&gt; s.ordering()))
            Add(s);

        return groups;
    }
}


Here's a sample class to group by distance from center of current groups:

public class gRectangleGroup : IGGrouping&lt;Rectangle&gt; {
    List&lt;Rectangle&gt; members;
    public Point center;

    public gRectangleGroup() {
        members = new List&lt;Rectangle&gt;();
    }

    public gRectangleGroup Add(Rectangle r) {
        members.Add(r);
        center = new Point(members.Average(m =&gt; m.Loc.X), members.Average(m =&gt; m.Loc.Y));
        return this;
    }

    public bool BelongsToGroup(Rectangle r) =&gt; center.Distance(r.Loc) &lt;= 5;

    public Rectangle Middle() =&gt; members.OrderBy(m =&gt; m.Loc.Distance(center)).First();

    public IEnumerator&lt;Rectangle&gt; GetEnumerator() =&gt; members.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator();

    IGGrouping&lt;Rectangle&gt; IGGrouping&lt;Rectangle&gt;.Add(Rectangle aT) =&gt; Add(aT);
}


You use gRectangleGroup like so:

List&lt;Rectangle&gt; list;

var ans2 = list.GroupBy&lt;gRectangleGroup, Rectangle&gt;().Cast&lt;gRectangleGroup&gt;().Select(rg =&gt; rg.Middle());


I don't like having to list the group type and the element type explicitly in the call to GroupBy. I don't like the Cast to gRectangleGroup when the type is passed to the GroupBy, but if GroupBy returns TG then I have to give up the fluent Add call for new groups.

I also don't like how a class that implements IGGrouping (like gRectangleGroup) and has a fluent Add that returns this, must also have another Add to return IGGrouping&lt;T&gt; because of the interface.

Can anyone suggest how this might be improved, or what they would change to solve the same issue?
You don't need the type parameter on IOrderable:

  public interface IOrderable
  {
    double Ordering();
  }




I don't like that you sort the source vector without notifying the client. In this way mysterious things are going on and the method does more than one thing. At least you should have a flag argument that lets the client determine if the source should be sorted or not before grouping:

public static IEnumerable&lt;IGGrouping&lt;T&gt;&gt; GroupBy&lt;TG, T&gt;(this IEnumerable&lt;T&gt; src, bool sortBeforeGrouping = false) where TG : IGGrouping&lt;T&gt;, new() where T : IOrderable
{
  var groups = new List&lt;IGGrouping&lt;T&gt;&gt;();

  ...
}


Alternatively you should rename the method to SortAndGroupBy(...). But I would let it total up to the client if the source should be sorted or not.



I think the inner of the internal Add(...) function can be simplified a bit:

  void Add(T aT)
  {
    var group = groups.FirstOrDefault(g =&gt; g.BelongsToGroup(aT));
    if (group == null)
    {
      group = new TG();
      groups.Add(group);
    }
    group.Add(aT);
  }




On way to get rid of the custom interfaces is to let the group type implement ICollection&lt;T&gt; and then inject the belongsToGroup function:

public static IEnumerable&lt;TKey&gt; GroupBy&lt;TKey, TElement&gt;(this IEnumerable&lt;TElement&gt; source, Func&lt;TElement, TKey, bool&gt; belongsToGroup) where TKey : ICollection&lt;TElement&gt;, new()
{
  return source.Aggregate(new List&lt;TKey&gt;(), (groups, e) =&gt;
  {
    TKey group = groups.FirstOrDefault(g =&gt; belongsToGroup(e, g));
    if (group == null)
    {
      group = new TKey();
      groups.Add(group);
    }
    group.Add(e);
    return groups;
  });

}


Usage

  Func&lt;Rectangle, RectangleGroup, bool&gt; belongsToGroup = (r, gr) =&gt; gr.BelongsToGroup(r);
  var midPoints = list.OrderBy(r =&gt; r).GroupBy(belongsToGroup).Select(gr =&gt; gr.Middle());
  foreach (var middle in midPoints)
  {
    Console.WriteLine(middle);
  }




If that isn't good enough then you can inject the addToGroup function too:

public static IEnumerable&lt;TKey&gt; GroupBy&lt;TKey, TElement&gt;(this IEnumerable&lt;TElement&gt; source, Func&lt;TElement, TKey, bool&gt; belongsToGroup, Action&lt;TElement, TKey&gt; addToGroup) where TKey : new()
{
  List&lt;TKey&gt; groups = new List&lt;TKey&gt;();

  foreach (TElement element in source)
  {
    TKey group = groups.FirstOrDefault(g =&gt; belongsToGroup(element, g));
    if (group == null)
    {
      group = new TKey();
      groups.Add(group);
    }

    addToGroup(element, group);
  }

  return groups;
}


Usage

  Func&lt;Rectangle, RectangleGroup, bool&gt; belongsToGroup = (r, gr) =&gt; gr.BelongsToGroup(r);
  Action&lt;Rectangle, RectangleGroup&gt; adder = (r, gr) =&gt; gr.Add(r);
  var midPoints= list.OrderBy(r =&gt; r).GroupBy(belongsToGroup, adder).Select(gr =&gt; gr.Middle());
  foreach (var middle in midPoints)
  {
    Console.WriteLine(middle);
  }