グループ化してセット演算子を適用する。 4

さらに再考。
ILookupに変換する部分を抽出。これでもうすこし使い勝手が良くなる。

■グループ化セット演算

public static class GroupLinqExtensions
{
  // groupのシーケンスをlookupに変換
  public static ILookup<TKey, T> GroupToLookup<T, TKey>(this IEnumerable<IGrouping<TKey, T>> source)
  {
    return source.SelectMany(x => x.Select(y => new { key = x.Key, value = y })).ToLookup(x => x.key, x => x.value);
  }

  // グループ化したキーで重複除去
  public static IEnumerable<T> GroupDistinct<T, TKey>(this IEnumerable<IGrouping<TKey, T>> source)
  {
    return source.GroupToLookup().Select(x => x.First());
  }

  // グループ化したキーで和
  public static IEnumerable<IGrouping<TKey, T>> GroupUnion<T, TKey>(this IEnumerable<IGrouping<TKey, T>> first, IEnumerable<IGrouping<TKey, T>> second)
  {
    var unionedKeys = first.Select(x => x.Key).Union(second.Select(x => x.Key));
    return first.Union(second).Where(x => unionedKeys.Contains(x.Key));
  }

  // グループ化したキーで積
  public static IEnumerable<IGrouping<TKey, T>> GroupIntersect<T, TKey>(this IEnumerable<IGrouping<TKey, T>> first, IEnumerable<IGrouping<TKey, T>> second, bool onlyFirstSource = false)
  {
    var intersectedKeys = first.Select(x => x.Key).Intersect(second.Select(x => x.Key));
    var srcGroup = onlyFirstSource ? first : first.Union(second);
    return srcGroup.Where(x => intersectedKeys.Contains(x.Key));
  }

  // グループ化したキーで差
  public static IEnumerable<IGrouping<TKey, T>> GroupExcept<T, TKey>(this IEnumerable<IGrouping<TKey, T>> first, IEnumerable<IGrouping<TKey, T>> second)
  {
    var exceptedKeys = first.Select(x => x.Key).Except(second.Select(x => x.Key));
    return first.Where(x => exceptedKeys.Contains(x.Key));
  }
}