1. Introduction
When you're working with just a "flat" list of objects (like our list of products or users from previous lectures), it's pretty straightforward: filter, transform, sort. But in real business apps, at interviews (and even in homework!), you often run into collections that contain other collections inside.
For example, there's a User class that has a list of orders. Or, say, a Product class with a list of reviews. Now here's the challenge: how do you get a list of all orders from all users? Or collect all reviews for all products? And what if you need a list of all products in all orders?
In these situations, regular Select and even C#'s magic sometimes just isn't enough. That's where our hero comes in — the SelectMany operator, turning a "list of lists" into just a "list".
Kitchen analogy
Imagine you have a box, with more boxes inside, and in those boxes — cookies. Your job is to dump all the cookies into one big bowl. You wouldn't take each box and pour out one cookie at a time, you'd just grab all the boxes and all the cookies — right into the bowl.
That's exactly what SelectMany does: it "flattens" the collection, turning a "box of boxes" into one "bowl" with all the goodies.
2. Let's recall our app example
In previous lectures, we built a simple app to work with users and products. For this lecture, let's add some "nested" collections.
Suppose we have these classes:
public class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
public string Name { get; set; }
}
Now let's say we have a list of users, each with a list of orders, and each order has a list of products.
How do you get all products from all orders of all users?
Suppose we want to make a master list of all products — no nesting, just one big "bowl". What if we try regular Select?
List<User> users = ...; // let's say we already have the data somewhere
var allOrders = users.Select(u => u.Orders);
The type of allOrders will be... IEnumerable<List<Order>>. We got a "list of lists", meaning a bunch of order collections. What we want is one flat list of all orders.
Let's try again:
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
What now? Now we've got a "list of lists of lists"! We're just making the structure more complicated, not simpler.
3. The solution: SelectMany operator
Here's where the real power of SelectMany comes in. Its job is to "flatten" nested collections into one.
General syntax:
collection.SelectMany(item => item.InnerCollection)
In our case:
var allOrders = users.SelectMany(u => u.Orders);
// Type: IEnumerable<Order>
Now we've just got a sequence of all orders from all users.
Let's go even deeper! All products from all orders of all users:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// Type: IEnumerable<Product>
What about in one line?
Yep, you can do that too:
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
It's better to read this chain in parts, especially if you're not a robot.
4. Let's break it down with a big example
Let's create some test data and see how SelectMany works in action.
var users = new List<User>
{
new User
{
Name = "Alice",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "Cookie" },
new Product { Name = "Milk" },
}
},
new Order
{
Id = 2,
Products = new List<Product>
{
new Product { Name = "Chocolate" }
}
}
}
},
new User
{
Name = "Bob",
Orders = new List<Order>
{
new Order
{
Id = 3,
Products = new List<Product>
{
new Product { Name = "Coffee" },
new Product { Name = "Tea" }
}
}
}
}
};
"Dumping" all products into one bowl
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
Program output:
Cookie
Milk
Chocolate
Coffee
Tea
What's happening at each step?
- users.SelectMany(u => u.Orders) — we take all users and "unroll" their orders into one collection of orders (now we've got 3 orders).
- .SelectMany(o => o.Products) — unroll the list of products from all orders.
Heads up: If we used regular Select, we'd get a collection of collections (like IEnumerable<List<Product>>). But with SelectMany we get a "flat" collection of products: IEnumerable<Product>.
5. What it looks like in a diagram
Before SelectMany
users (List<User>)
└── User #1
│ └── Orders (List<Order>)
│ ├── Order #1 -> Products (List<Product>)
│ └── Order #2 -> Products (List<Product>)
└── User #2
└── Orders (List<Order>)
└── Order #3 -> Products (List<Product>)
After the first SelectMany(u => u.Orders)
IEnumerable<Order>: [Order #1, Order #2, Order #3]
After the second SelectMany(o => o.Products)
IEnumerable<Product>: [Cookie, Milk, Chocolate, Coffee, Tea]
6. Comparison: Select vs SelectMany
| Select | SelectMany | |
|---|---|---|
| Returns | Collection of "collections" | Flat collection |
| Example | → |
→ |
| Use case | If you want to keep the nested structure | If you want one flat sequence |
Table: when to use which operator
| What you want to do | What to use | Result |
|---|---|---|
| Get a list of lists (don't flatten) | Select | |
| Get one "flat" sequence | SelectMany | |
| Get parent-child pairs | SelectMany with result selector | |
| Just transform elements of a "flat" collection | Select | |
7. Handy tips
Query Syntax (for fans of "SQL-style")
For those who love expressiveness — LINQ lets you write queries in a syntax that looks like SQL:
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
This code does the same thing as the SelectMany chain, just looks like a multi-level "from".
"Flattening" a matrix
A classic use case for SelectMany is working with a two-dimensional array or a list of lists.
List<List<int>> matrix = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5 },
new List<int> { 6 }
};
How do you get one list of all the numbers?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. Advanced syntax
Sometimes, using an overload of SelectMany, you can return not just the inner collection's element, but also info from the outer one.
Here's an example: you need to get pairs "User name — Product name" for every product in every order.
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(
order => order.Products,
(order, product) => new { UserName = user.Name, ProductName = product.Name }
)
);
foreach (var pair in userProductPairs)
{
Console.WriteLine($"{pair.UserName} ordered {pair.ProductName}");
}
Here, the second lambda parameter is the outer collection's element. Super useful so you don't "lose" info from the outer levels.
9. Practical scenarios
Lots of orders, lots of products
You're a dev for an online store. Want to count how many unique products all clients bought? Here you go:
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
Building a flat collection from a hierarchy
You've got a list of company departments, each with employees. Want a list of all employees in the company:
var allEmployees = departments.SelectMany(d => d.Employees);
Working with strings: "flattening" an array of words into characters
string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
Console.Write(c + " "); // h e l l o w o r l d
}
10. Common mistakes and gotchas
Now that you're pumped about the power of SelectMany, it's time for a few common mistakes to watch out for.
The most popular one — expecting Select to give you a "flat" collection. In reality, it always returns a collection of collections if your inner function returns a collection. So you end up writing double loops or getting lost in types.
Second mistake — not noticing that "flattening" makes you lose info about the outer element: for example, if you're only working with products, you won't know which order or user it came from unless you use the overload with a result selector.
Third gotcha — if your inner lists might be null, add some protection before using SelectMany: either filter or replace with an empty list, otherwise you'll get a NullReferenceException faster than you can hit F5.
GO TO FULL VERSION