1

This is a build off of How to flatten nested objects with linq expression as I don't have enough reputation to add comments to the discussion.

I'm trying to flatten a tree structure of different types into a single list.

Here is the sample model:

public class Book
{
    public string Name { get; set; }
    public IList<Chapter> Chapters { get; set; }
}

public class Chapter
{
    public string Name { get; set; }
    public IList<Page> Pages { get; set; }
}


public class Page
{
    public string Name { get; set; }
}

And sample data:

Book: Pro Linq 
{ 
   Chapter 1: Hello Linq 
   {
      Page 1, 
      Page 2, 
      Page 3
   },
   Chapter 2: C# Language enhancements
   {
      Page 4
   },
   Chapter 3: Glossary
   {
   }
}
Book: Pro Linq II
{
}

And the desired flat output:

"Pro Linq", "Hello Linq", "Page 1"
"Pro Linq", "Hello Linq", "Page 2"
"Pro Linq", "Hello Linq", "Page 3"
"Pro Linq", "C# Language enhancements", "Page 4"
"Pro Linq", "Glossary", null
"Pro Linq II", null, null

Is there any way to do this without concat and without processing the collection twice?

nr-91
  • 333
  • 4
  • 14

4 Answers4

1

I'm guessing there is a cleaner way to accomplish this, but the following will get your desired output based on the input. I used the null coalescing operator to turn nulls into the string "null" for ease of printing, you can just as easily leave them null.

var result =
from book in books
from chapter in (book?.Chapters?.DefaultIfEmpty(new Chapter()) ?? Enumerable.Repeat<Chapter>(new Chapter(),1))
from page in (chapter?.Pages?.DefaultIfEmpty(new Page()) ?? Enumerable.Repeat<Page>(new Page(),1))
select new {Book = book?.Name ?? "null", Chapter = chapter?.Name ?? "null", Page = page?.Name ?? "null"};

And the output

Pro Linq, Hello Linq, Page 1
Pro Linq, Hello Linq, Page 2
Pro Linq, Hello Linq, Page 3
Pro Linq, C# Language Enhancements, Page 4
Pro Linq, Glossary, null
Pro Linq II, null, null

Additionally, here's the input using the types provided in the question should another user want to experiment without building this out themselves.

var books = new List<Book> {
    new Book{
        Name = "Pro Linq",
        Chapters = new List<Chapter>{
            new Chapter {
                Name = "Hello Linq",
                Pages = new List<Page> {
                    new Page {Name = "Page 1"},
                    new Page {Name = "Page 2"},
                    new Page {Name = "Page 3"}
                }
            },
            new Chapter {
                Name = "C# Language Enhancements",
                Pages = new List<Page> {
                    new Page {Name = "Page 4"}
                }
            },
            new Chapter {
                Name = "Glossary"
            }
        }
    },
    new Book {
        Name = "Pro Linq II"
    }
};
Jonathon Chase
  • 9,396
  • 21
  • 39
0

While not pretty it does produce the desired result.

Using SelectMany along with some inline if statements to check for null.

var flattenedList = books.SelectMany(book =>
    book.Chapters != null && book.Chapters.Count > 0
     ? book.Chapters.SelectMany(chapter =>
         chapter.Pages != null && chapter.Pages.Count > 0
            ? chapter.Pages.Select(page => string.Join(", ", book.Name, chapter.Name, page.Name))
            : new[] { string.Join(", ", book.Name, chapter.Name, "null") }
    )
    : new[] { string.Join(", ", book.Name, "null", "null") }
).ToList();
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

This answer based upon Nkosi's answer with a few very minor tweaks.

  • Use of Any()
  • No ToList()
  • Func added and used to surround text with quotes

The code:

Func<string, string> Quote = (s) => "\"" + s + "\"";
var result = books.SelectMany(book =>
    book.Chapters != null && book.Chapters.Any()
     ? book.Chapters.SelectMany(chapter =>
         chapter.Pages != null && chapter.Pages.Any()
            ? chapter.Pages.Select(page => string.Join(", ", Quote(book.Name), Quote(chapter.Name), Quote(page.Name)))
            : new[] { string.Join(", ", Quote(book.Name), Quote(chapter.Name), "null") }
    )
    : new[] { string.Join(", ", Quote(book.Name), "null", "null") }
);

This produces:

"Pro Linq", "Hello Linq", "Page 1"
"Pro Linq", "Hello Linq", "Page 2"
"Pro Linq", "Hello Linq", "Page 3"
"Pro Linq", "C# Language Enhancements", "Page 4"
"Pro Linq", "Glossary", null
"Pro Linq II", null, null
MEC
  • 1,690
  • 1
  • 17
  • 23
0

Using a string extension for quoting makes this cleaner:

public static class Ext {
    public static string Quoted(this string text) => $"\"{text}\"";
}

You need to replace the null items with IEnumerable<>s that return a single null item like so:

var ans = myBooks.SelectMany(b => (b.Chapters == null ? new[] { (Chapter)null } : b.Chapters)
                                      .SelectMany(c => (c?.Pages == null ? new[] { (Page)null } : c.Pages)
                                                           .Select(p => $"{b.Name.Quoted()}, {c?.Name?.Quoted() ?? "null"}, {p?.Name?.Quoted() ?? "null"}")
                                                 )
                            );
NetMage
  • 26,163
  • 3
  • 34
  • 55