Flattern W tym wpisie zobaczymy użycie metod SelectMany w LINQ. 

SelectMany w pewnym sensie jest odwrotnością GroupBy. GroupBy bierze jedną kolekcję i transformuje to odpowiednio na kolekcje, w której elementy mają swoje swoją kolekcje.

Natomiast SelectMany spłaszcza takie kolekcje w kolekcji do jednej scalonej listy elementów

Ponownie wyobraź sobie, że mamy kolekcje wpisów na bloga. Tym razem jednak mamy także definicję samego bloga. Będziemy mieć dwa blogi o przemawianiu i o programowaniu.

public class Blog
{
    public IEnumerable<BlogPost> Posts { get; set; }

    public string Title { get; set; }
}

public class BlogPost
{
    public string Title { get; set; }

    public string Author { get; set; }

    public string Subject { get; set; }
}

Do przykładu stworzyłem także statyczne klasy, które będą generować kolekcję wpisów dla tych dwóch blogów.

public static class BlogPostsAboutPrograming
{
    public static string GetJson()
    {
        var list = Get();
        return Newtonsoft.Json.JsonConvert.SerializeObject(list);
    }

    public static IEnumerable<BlogPost> Get()
    {
        List<BlogPost> blogPosts = new List<BlogPost>();

        BlogPost p1 = new BlogPost()
        {
            Title = "Kuberetes i Docker : Wytłumacz mi i pokaż",
            Author = "Jacke",
            Subject = "Kubernetes"
        };

        BlogPost p2 = new BlogPost()
        {
            Title = "Zakup mieszkania w Warszawie : Zarobki programisty",
            Author = "Damian",
            Subject = "Filozfia"
        };

        BlogPost p3 = new BlogPost()
        {
            Title = "Wypoczęty programista. CO ? : Aktywny programista",
            Author = "Damian",
            Subject = "Filozfia"
        };

        BlogPost p4 = new BlogPost()
        {
            Title = "Zakładanie Rodziny czy Kariera czy Marzenia : Serce Programisty",
            Author = "Damian",
            Subject = "Filozfia"
        };

        BlogPost p5 = new BlogPost()
        {
            Title = "Sukces? To gra długodystansowa : Aktywny programista",
            Author = "Damian",
            Subject = "Filozfia"
        };

        BlogPost p6 = new BlogPost()
        {
            Title = "Co nowego w C# 9.0 ? Rekordy i Pattern Matching",
            Author = "Cezary",
            Subject = "c#"
        };

        BlogPost p7 = new BlogPost()
        {
            Title = "Asynchroniczny C# : Awaitables, Maszyna stanów i TaskCompletionSource",
            Author = "Cezary",
            Subject = "C#"
        };

        BlogPost p8 = new BlogPost()
        {
            Title = "TDD z C# : Test jednostkowy z XUnit, NUnit, MSUnit",
            Author = "Cezary",
            Subject = "c#"
        };

        BlogPost p9 = new BlogPost()
        {
            Title = "Higher Order Function : Przykłady Funkcji wyższego rzędu w C#",
            Author = "Cezary",
            Subject = "Csharp"
        };

        BlogPost p10 = new BlogPost()
        {
            Title = "Mailchimp i ASP.NET : Stworzenie newslettera",
            Author = "Kamil",
            Subject = "ASP.NET"
        };

        BlogPost p11 = new BlogPost()
        {
            Title = "ASP.NET i Angular : Caching, konfiguracja, REST API, Components",
            Author = "Adam",
            Subject = "asp net"
        };

        BlogPost p12 = new BlogPost()
        {
            Title = "Kurs ASP.NET i Angular : Zobaczmy domyślny szablon",
            Author = "Adam",
            Subject = "ASP.NET"
        };

        BlogPost p13 = new BlogPost()
        {
            Title = "ASP.NET Core Response Compression na Blog",
            Author = "Kamil",
            Subject = "ASP.NET"
        };

        blogPosts.Add(p1); blogPosts.Add(p2);
        blogPosts.Add(p3); blogPosts.Add(p4);
        blogPosts.Add(p5); blogPosts.Add(p6);
        blogPosts.Add(p7); blogPosts.Add(p8);
        blogPosts.Add(p9); blogPosts.Add(p10);
        blogPosts.Add(p11); blogPosts.Add(p12);
        blogPosts.Add(p13);

        return blogPosts;
    }
}

public static class BlogPostsAboutSpeeaking
{
    public static string GetJson()
    {
        var list = Get();
        return Newtonsoft.Json.JsonConvert.SerializeObject(list);
    }

    public static IEnumerable<BlogPost> Get()
    {
        List<BlogPost> blogPosts = new List<BlogPost>();

        BlogPost p1 = new BlogPost()
        {
            Title = "JAK COVID zmienił świat przemawiania",
            Author = "Jacke",
            Subject = "Przemawianie"
        };

        BlogPost p2 = new BlogPost()
        {
            Title = "SŁUCHANIE i rzeczywiste powody by nauczyć się słuchać",
            Author = "Damian",
            Subject = "Przemawianie"
        };

        BlogPost p3 = new BlogPost()
        {
            Title = "Pisać czy nie pisać mowy DLACZEGO",
            Author = "Damian",
            Subject = "Przemawianie"
        };

        BlogPost p4 = new BlogPost()
        {
            Title = "Jordan Peterson i posprzątaj swój POKÓJ",
            Author = "Damian",
            Subject = "Samorozwój"
        };

        BlogPost p5 = new BlogPost()
        {
            Title = "5 obaw z którymi trzeba się ZMIERZYĆ",
            Author = "Damian",
            Subject = "Samorozwój"
        };

        BlogPost p6 = new BlogPost()
        {
            Title = "Mentorować JAK I PO CO ? ",
            Author = "Cezary",
            Subject = "Samorozwój"
        };

        BlogPost p7 = new BlogPost()
        {
            Title = "4 typy CHARYZMY",
            Author = "Cezary",
            Subject = "Książki"
        };

        BlogPost p8 = new BlogPost()
        {
            Title = "Fight Club i gdzie jest współczesna MĘSKOŚĆ",
            Author = "Cezary",
            Subject = "Książki"
        };

        BlogPost p9 = new BlogPost()
        {
            Title = "TEORIA przywiązania WEDŁUG KSIĄŻKI ATTACHED AUTORÓW RACHEL S.F.HELLER,AMIR LEVINE",
            Author = "Cezary",
            Subject = "Książki"
        };

        BlogPost p10 = new BlogPost()
        {
            Title = "32 pomysłów na tworzenie ARGUMENTÓW",
            Author = "Kamil",
            Subject = "Przemawianie"
        };

        BlogPost p11 = new BlogPost()
        {
            Title = "The COMPOUND EFFECT Codzienne działanie, masowe REZULTATY",
            Author = "Adam",
            Subject = "Książki"
        };

        BlogPost p12 = new BlogPost()
        {
            Title = "Ekstaza i koncentracja stanu FLOW",
            Author = "Adam",
            Subject = "Książki"
        };


        blogPosts.Add(p1); blogPosts.Add(p2);
        blogPosts.Add(p3); blogPosts.Add(p4);
        blogPosts.Add(p5); blogPosts.Add(p6);
        blogPosts.Add(p7); blogPosts.Add(p8);
        blogPosts.Add(p9); blogPosts.Add(p10);
        blogPosts.Add(p11); blogPosts.Add(p12);

        return blogPosts;
    }
}

Stwórzmy teraz kolekcję, która będzie miała te dwa blogi.

Blog blogprograming = new Blog()
{
    Title = "Programowanie jest Cool",
    Posts = BlogPostsAboutPrograming.Get()
};

Blog blogspeaking = new Blog()
{
    Title = "Przemawianie to Moc",
    Posts = BlogPostsAboutSpeeaking.Get()
};

List<Blog> blogs = new List<Blog>();
blogs.Add(blogprograming);
blogs.Add(blogspeaking);

No to mamy teraz złożoną kolekcję blogów. Każdy bloga ma także wpisy.

Przy użyciu SelectMany chcielibyśmy stworzyć jedną kolekcję wszystkich wpisów z tych blogów.

var posts = blogs.SelectMany(k => k.Posts);

var json = Newtonsoft.Json.JsonConvert.SerializeObject(posts);

Warto tutaj skorzystać z metody Where, gdyż tych przykładowych wpisów jest za dużo.

var posts = blogs.SelectMany(k => k.Posts).
    Where(w => w.Author == "Cezary" && w.Author == "Jacke");

var json = Newtonsoft.Json.JsonConvert.SerializeObject(posts);

Oto JSON, który pokaże wpisy z dwóch blogów, ale tego samego autora.

[
   {
      "Title":"Kuberetes i Docker : Wytłumacz mi i pokaż",
      "Author":"Jacke",
      "Subject":"Kubernetes"
   },
   {
      "Title":"JAK COVID zmienił świat przemawiania",
      "Author":"Jacke",
      "Subject":"Przemawianie"
   }
]

Gdybym chciał utworzyć listę tematów wszystkich blogów to wtedy bym musiał zagnieździć metodę Select do tego. Distinct wyeliminowałoby by powtórzenie się tematów.

var subjects = blogs.SelectMany(k => k.Posts).
    Select(l => l.Subject).Distinct();

var json = Newtonsoft.Json.JsonConvert.SerializeObject(subjects);

Oto JSON tego rezultatu :

["Kubernetes","Filozfia","c#","C#","Csharp","ASP.NET","asp net","Przemawianie","Samorozwój","Książki"]

To są podstawy metody SelectMany. Jak widzisz rzeczywiście jest to metoda odwrotna do GroupBy. 

Podobnie jak z GroupBy możemy określić co dokładnie SelectMany powinien zwrócić korzystając z przeciążonej wersji metody SelectMany.

W niej możemy dodać wyrażenie lambda, które zwróci nowy typ anonimowy bądź konkretną klasę. Ten typ anonimowy będzie zawierać nazwę bloga oraz nazwę poszczególnego wpisu wraz z jego autorem.

var result = blogs.SelectMany(k => k.Posts, (blog, post) => new
{
    BlogName = blog.Title,
    PostTitle = post.Title,
    Author = post.Author
})
.Where(ano => ano.Author == "Jacke");

var json5 = Newtonsoft.Json.JsonConvert.SerializeObject(result);

Oto jak wygląda ta kolekcja.

[
   {
      "Title":"Kuberetes i Docker : Wytłumacz mi i pokaż",
      "Author":"Jacke",
      "Subject":"Kubernetes"
   },
   {
      "Title":"JAK COVID zmienił świat przemawiania",
      "Author":"Jacke",
      "Subject":"Przemawianie"
   }
]

Swoją drogą kto powiedział, że musisz zwracać jakieś obiekty. Dlaczego nie opakować te wszystkie informacje w napisach string?

var resultstring = blogs.SelectMany(k => k.Posts, (blog, post) =>
$"{post.Title} : Napisany przez {post.Author} na blogu {blog.Title}");

var json6 = Newtonsoft.Json.JsonConvert.SerializeObject(resultstring);

Teraz możesz odczytać wszystkie wartości w tym JSON.

[
   "Kuberetes i Docker : Wytłumacz mi i pokaż : Napisany przez Jacke na blogu Programowanie jest Cool",
   "Zakup mieszkania w Warszawie : Zarobki programisty : Napisany przez Damian na blogu Programowanie jest Cool",
   "Wypoczęty programista. CO ? : Aktywny programista : Napisany przez Damian na blogu Programowanie jest Cool",
   "Zakładanie Rodziny czy Kariera czy Marzenia : Serce Programisty : Napisany przez Damian na blogu Programowanie jest Cool",
   "Sukces? To gra długodystansowa : Aktywny programista : Napisany przez Damian na blogu Programowanie jest Cool",
   "Co nowego w C# 9.0 ? Rekordy i Pattern Matching : Napisany przez Cezary na blogu Programowanie jest Cool",
   "Asynchroniczny C# : Awaitables, Maszyna stanów i TaskCompletionSource : Napisany przez Cezary na blogu Programowanie jest Cool",
   "TDD z C# : Test jednostkowy z XUnit, NUnit, MSUnit : Napisany przez Cezary na blogu Programowanie jest Cool",
   "Higher Order Function : Przykłady Funkcji wyższego rzędu w C# : Napisany przez Cezary na blogu Programowanie jest Cool",
   "Mailchimp i ASP.NET : Stworzenie newslettera : Napisany przez Kamil na blogu Programowanie jest Cool",
   "ASP.NET i Angular : Caching, konfiguracja, REST API, Components : Napisany przez Adam na blogu Programowanie jest Cool",
   "Kurs ASP.NET i Angular : Zobaczmy domyślny szablon : Napisany przez Adam na blogu Programowanie jest Cool",
   "ASP.NET Core Response Compression na Blog : Napisany przez Kamil na blogu Programowanie jest Cool",
   "JAK COVID zmienił świat przemawiania : Napisany przez Jacke na blogu Przemawianie to Moc",
   "SŁUCHANIE i rzeczywiste powody by nauczyć się słuchać : Napisany przez Damian na blogu Przemawianie to Moc",
   "Pisać czy nie pisać mowy DLACZEGO : Napisany przez Damian na blogu Przemawianie to Moc",
   "Jordan Peterson i posprzątaj swój POKÓJ : Napisany przez Damian na blogu Przemawianie to Moc",
   "5 obaw z którymi trzeba się ZMIERZYĆ : Napisany przez Damian na blogu Przemawianie to Moc",
   "Mentorować JAK I PO CO ?  : Napisany przez Cezary na blogu Przemawianie to Moc",
   "4 typy CHARYZMY : Napisany przez Cezary na blogu Przemawianie to Moc",
   "Fight Club i gdzie jest współczesna MĘSKOŚĆ : Napisany przez Cezary na blogu Przemawianie to Moc",
   "TEORIA przywiązania WEDŁUG KSIĄŻKI ATTACHED AUTORÓW RACHEL S.F.HELLER,AMIR LEVINE : Napisany przez Cezary na blogu Przemawianie to Moc",
   "32 pomysłów na tworzenie ARGUMENTÓW : Napisany przez Kamil na blogu Przemawianie to Moc",
   "The COMPOUND EFFECT Codzienne działanie, masowe REZULTATY : Napisany przez Adam na blogu Przemawianie to Moc",
   "Ekstaza i koncentracja stanu FLOW : Napisany przez Adam na blogu Przemawianie to Moc"
]

Jak widzisz spłaszczanie kolekcji w kolekcji może być całkiem fajne.

Co jeszcze SelectMany potrafi?

Otóż możesz na podstawie indeksu z jednej kolekcji możesz wykonać połączenie do drugiej. Ciężko było mi stworzyć sensowny przykład użycia. To była nawet najdłuższa część tego wpisu 

Najpierw muszę stworzyć kolejne przykładowe obiekty

public static class BlogPostsAboutGames
{
    public static IEnumerable<BlogPost> Get()
    {
        List<BlogPost> blogPosts = new List<BlogPost>();

        BlogPost p1 = new BlogPost()
        {
            Title = "Final Fantasy 7",
            Author = "Jacke",
            Subject = "RPG"
        };

        blogPosts.Add(p1);

        return blogPosts;
    }

    public static string GetJson()
    {
        var list = Get();
        return Newtonsoft.Json.JsonConvert.SerializeObject(list);
    }
}
var aboutgames = BlogPostsAboutGames.Get();
Blog blogGame = new Blog()
{
    Title = "Gram to i owo",
    Posts = aboutgames
};
List<Blog> blogs2 = new List<Blog>();
blogs2.Add(blogGame);
blogs2.Add(blogGame);

Teraz gdy mamy listę różnych blogów mogę po indeksie je połączyć w taki sposób :

var resultIndex = blogs.SelectMany(
    (blog, index) =>
    blogs2[index].Posts,
    (blog, blogpost) =>
 new
 {
     BlogName = blog.Title,
     Posts = blogpost
 });

var json7 = Newtonsoft.Json.JsonConvert.SerializeObject(resultIndex);

Oto JSON tego rezultatu :

[
   {
      "BlogName":"Programowanie jest Cool",
      "Posts":{
         "Title":"Final Fantasy 7",
         "Author":"Jacke",
         "Subject":"RPG"
      }
   },
   {
      "BlogName":"Przemawianie to Moc",
      "Posts":{
         "Title":"Final Fantasy 7",
         "Author":"Jacke",
         "Subject":"RPG"
      }
   }
]

Jak widzisz definicja starej listy blogowej nałożyła się na listę wpisów z nowej listy blogów na temat gier.

Podsumowując SelectMany jest ciekawą metodą spłacająca. Jako początkujący programista miałem problem ze zrozumieniem jak ona działa, bo nie znalazłem dobrego przykładu. Mam nadzieje, że ten wpis Ci pomoże.