C# LINQ - Group List by a property then select by different groups

Viewed 5396

I have a list that I want to group by property A. Then select all the elements for that group where the GroupBy key is null or '' then I want to return all the elements for those groups. Otherwise, for a group key return the first element of that group. I have come up with the following and what I expect from code. Any help would be much appreciated.

Sample code:

public class ABC
{
    public string A { get; set; }
    public string B { get; set; }
    public string C { get; set; }
}

var list = new List<ABC>()
{
    new ABC() { A = "a", B = "b", C = "c" },
    new ABC() { A = "a", B = "b", C = "c" },
    new ABC() { A = null, B = "b", C = "c" },
    new ABC() { A = null, B = "b", C = "c" },
    new ABC() { A = "", B = "b", C = "c" },
    new ABC() { A = "", B = "b", C = "c" },
};

var result = list.GroupBy(x => x.A)
                 .Select(x => 
                 {
                     if (!string.IsNullOrEmpty(x.Key))
                        return x.First();
                     else
                        return x;
                 })
                 .ToList();

What I expect:

I am expecting to see a total of 5 count from the result

{ A = "a", B = "b", C = "c" } count 1 (1 removed)
{ A = null, B = "b", C = "c" } count 2 (none removed)
{ A = "", B = "b", C = "c" } count 2 (none removed)
4 Answers

Something like this:

 var result = list
   .GroupBy(item => item.A)
   .SelectMany(group => string.IsNullOrEmpty(group.Key)
      ? group as IEnumerable<ABC>
      : new ABC[] {group.First()})
   .ToList();

The trick here is that we should return collection: either entire group or a collection (array) with the First item only.

You can get rid of grouping and flatten to have a faster version which, however, exploits side effect:

 HashSet<string> uniqueA = new HashSet<string>(); 

 var result = list
   .Where(item => string.IsNullOrEmpty(item.A) || uniqueA.Add(item.A))
   .ToList(); 

A few pointers:

  • Optional, but consider a ternary operator instead of if/else.
  • If IsNullOrEmpty is false, instead of using First, use Where and filter for index of '0' so that it stays in list form, even though it only has one item in it.
  • Use SelectMany instead of Select to flatten the results

Here are the pointers in action:

var result = 
    list
    .GroupBy(x => x.A)
    .SelectMany(x => 
        string.IsNullOrEmpty(x.Key)
        ? x
        : x.Where((val,ix) => ix == 0)
    )
    .ToList();

A LINQPad Dump of this using your sample data produces:

A B C
a b c
null b c
null b c
b c
b c
var result = list.GroupBy(x => x.A)
                .Select(g =>
                    new
                    {
                        Key = g.Key,
                        Values = string.IsNullOrEmpty(g.Key) ? g.ToList() : new List<ABC> {g.FirstOrDefault()}
                    })
                .ToList();

Do you have to use LINQ? The answer by @pwilcox is fine but you might find something like this more readable:

var newList = new List<ABC>();
var setOfA = new HashSet<string>();
        
foreach (var abc in list)
{
    if (string.IsNullOrEmpty(abc.A))
    {
        newList.Add(abc);
    }
    else
    {
        if (setOfA.Contains(abc.A))
        {
            continue;
        }

        setOfA.Add(abc.A);
        newList.Add(abc);
    }
}
Related