Beyond the basic filter-sort-project operations, LINQ has powerful operators for reshaping data: flattening nested collections, grouping items into buckets, joining two sequences on matching keys, and pairing sequences element by element. These operators are the .NET equivalents of SQL’s INNER JOIN, GROUP BY, and CROSS JOIN — but expressed as composable method calls that work on any IEnumerable<T>.
SelectMany — Flattening Nested Collections
// Each post has a Tags array — we want all tags across all posts (flat)
var posts = new List<Post>
{
new Post { Title = "C# Tips", Tags = new[] { "csharp", "dotnet" } },
new Post { Title = "EF Guide", Tags = new[] { "dotnet", "ef-core", "sql" } },
new Post { Title = "Angular", Tags = new[] { "angular", "typescript" } },
};
// Select gives a list of arrays: string[][]
var nested = posts.Select(p => p.Tags);
// [["csharp","dotnet"], ["dotnet","ef-core","sql"], ["angular","typescript"]]
// SelectMany flattens into a single sequence: string[]
var allTags = posts.SelectMany(p => p.Tags);
// ["csharp","dotnet","dotnet","ef-core","sql","angular","typescript"]
// Combine with Distinct to get unique tags
var uniqueTags = posts.SelectMany(p => p.Tags).Distinct().OrderBy(t => t);
// ["angular","csharp","dotnet","ef-core","sql","typescript"]
// SelectMany with result selector (source item + collection item)
var tagPairs = posts.SelectMany(
p => p.Tags,
(post, tag) => new { post.Title, Tag = tag }
);
// { Title="C# Tips", Tag="csharp" }, { Title="C# Tips", Tag="dotnet" }, ...
Note:
SelectMany is equivalent to a nested foreach that yields individual items from inner collections. The name means “select and then flatten many results into one sequence.” It maps to SQL’s CROSS APPLY (and in some cases INNER JOIN) when used with EF Core. Whenever you find yourself writing foreach (var item in outer) foreach (var inner in item.Collection), consider whether SelectMany expresses the same intent more clearly.Tip:
GroupBy returns an IEnumerable<IGrouping<TKey, TElement>> — each group has a Key property and is itself an IEnumerable<TElement>. You can aggregate within each group: groups.Select(g => new { Category = g.Key, Count = g.Count(), Total = g.Sum(...) }). This pattern produces the summary rows you would get from a SQL GROUP BY with aggregate functions — commonly used for dashboard statistics and chart data in ASP.NET Core API responses.Warning:
Join in LINQ is an inner join — items from either source that have no matching key are excluded. Use GroupJoin followed by SelectMany with a default when you need a left outer join (keep all items from the left source, even those with no match on the right). In EF Core, prefer using navigation properties with Include() over manual LINQ joins — EF Core generates better SQL and the code is clearer.GroupBy — Grouping into Buckets
var orders = new List<Order>
{
new Order { CustomerId = "C1", Total = 50.0m },
new Order { CustomerId = "C2", Total = 120.0m },
new Order { CustomerId = "C1", Total = 75.0m },
new Order { CustomerId = "C3", Total = 200.0m },
new Order { CustomerId = "C2", Total = 45.0m },
};
// GroupBy — group orders by customer
var byCustomer = orders.GroupBy(o => o.CustomerId);
foreach (var group in byCustomer)
{
Console.WriteLine($"Customer {group.Key}: " +
$"{group.Count()} orders, £{group.Sum(o => o.Total):F2} total");
}
// Customer C1: 2 orders, £125.00 total
// Customer C2: 2 orders, £165.00 total
// Customer C3: 1 orders, £200.00 total
// Project groups into a summary object
var summaries = orders
.GroupBy(o => o.CustomerId)
.Select(g => new CustomerSummary
{
CustomerId = g.Key,
OrderCount = g.Count(),
TotalSpend = g.Sum(o => o.Total),
AverageOrder = g.Average(o => o.Total),
})
.OrderByDescending(s => s.TotalSpend)
.ToList();
Join — Combining Two Sequences
var authors = new List<Author>
{
new Author { Id = 1, Name = "Alice" },
new Author { Id = 2, Name = "Bob" },
new Author { Id = 3, Name = "Carol" },
};
var blogPosts = new List<Post>
{
new Post { AuthorId = 1, Title = "Hello World" },
new Post { AuthorId = 2, Title = "LINQ Guide" },
new Post { AuthorId = 1, Title = "C# Tips" },
};
// Join — inner join on matching keys
var postWithAuthor = blogPosts.Join(
authors,
post => post.AuthorId, // left key
author => author.Id, // right key
(post, author) => new // result selector
{
post.Title,
AuthorName = author.Name,
});
// { Title="Hello World", AuthorName="Alice" }
// { Title="LINQ Guide", AuthorName="Bob" }
// { Title="C# Tips", AuthorName="Alice" }
// In EF Core prefer navigation properties + Include over manual Join:
Common Mistakes
Mistake 1 — Using Select instead of SelectMany for nested collections
❌ Wrong — Select produces a list of arrays, not a flat list:
var tags = posts.Select(p => p.Tags); // IEnumerable<string[]> — nested!
✅ Correct — SelectMany flattens:
var tags = posts.SelectMany(p => p.Tags); // IEnumerable<string> — flat
Mistake 2 — Calling GroupBy without projecting the groups (iterating IGrouping multiple times)
❌ Wrong — groups evaluated twice, causing double enumeration:
✅ Correct — materialise groups immediately: .GroupBy(k).Select(g => new { Key = g.Key, Items = g.ToList() }).ToList().