Linq の SelectMany メソッドについて調べて、多少なりとも分かったような気になったので、自分なりの理解を備忘録として書いておくことにしました。

自分の手を動かしてコードを書くと理解が深まるだろうと思って、Northwind サンプルデータベースの Customers テーブルの顧客が過去に注文した製品の一覧を SelectMany メソッドと GroupBy メソッドを使って取得するサンプルを作ってみました。上の画像がその結果を表示したものです。どのようなコードを書いたかは後述します。
Microsoft のドキュメント「Enumerable.SelectMany メソッド」を見ると "シーケンスの各要素を IEnumerable<T> に射影し、結果のシーケンスを 1 つのシーケンスに平坦化します。 Projects each element of a sequence to an IEnumerable<T> and flattens the resulting sequences into one sequence." と書いてあるのですが、自分の頭ではその説明ではさっぱり意味が分かりませんでした。「シーケンス (sequence)」って何? 「射影 (project)」って何? 「平坦化 (flatten)」ってどういうこと?・・・って感じ。(汗)
ググって調べてみると「シーケンス」というのは .NET の Linq の世界に限れば "IEnumerable または IEnumerable<T> インターフェイスを継承するオブジェクト" と理解すれば良さそうです。
「射影」というのは Microsoft のドキュメント「射影操作 (C#)」によれば "オブジェクトを必要なプロパティだけで構成された別の形式に変換する操作" ということだそうです。その際「平坦化」を同時に行うのが Select メソッドとは違う所のようです。
で、問題の「平坦化」ですが、これは BuildInsider の記事「LINQ:取得列を明示的に指定する - select句/SelectManyメソッド[C#]」の説明が分かりやすかったです。
下の画像は Northwind サンプルデータベースの Customers, Orders, Order_Details, Products テーブルから生成した Entity Data Model ですが、これを例に取って説明します。

Orders の中には複数の顧客の注文データが複数(過去の注文の数)含まれており、各注文に紐づく詳細は Order_Details に含まれています。Order_Details のデータは Orders のナビゲーションプロパティ Order_Details から取得できます。
Orders から CustomerID が "ALFKI" の顧客の注文(Orders の中に複数あります)を抽出し、それに紐づく Order_Details を Select および SelectMany メソッドで引数にナビゲーションプロパティ Order_Details 設定して取得してみます。
Select メソッド
結果のオブジェクトが List<ICollection<OrderDetail>> 型となっています。上に紹介した BuildInsider の記事にも書いてありますように、OrderDetail にアクセスするためには 2 回ループを回す必要があります。

SelectMany メソッド
結果のオブジェクトが List<OrderDetail> 型になっており「平坦化」されているのが分かるでしょうか?

ちなみに SelectMany メソッドの引数に指定するナビゲーションプロパティは IEnumerable<T> 型でなければならないので注意してください。間違って他の Employee 型とかのプロパティを設定すると以下のようなエラーが出ます。(意味不明なので悩むかも。何を隠そう自分がそうでした)
"エラー CS0411 メソッド 'Enumerable.SelectMany<TSource, TResult>(IEnumerable<TSource>, Func<TSource, IEnumerable<TResult>>)' の型引数を使い方から推論することはできません。型引数を明示的に指定してください。"
もう一つ、上の例より実用的かもしれないサンプルコードを載せておきます。 ASP.NET Core MVC アプリで、Customers テーブル顧客一覧を表示し (Customers/Index)、一覧の中から選んだ特定の顧客が過去に注文した製品の一覧を SelectMany メソッドと GroupBy メソッドを使って取得し、ViewData を使って View に渡して表示するもので (Customers/Details)、この記事の一番上の画像がその結果です。
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcCore6App2.Data;
namespace MvcCore6App2.Controllers
{
public class CustomersController : Controller
{
private readonly NorthwindContext _context;
public CustomersController(NorthwindContext context)
{
_context = context;
}
public async Task<IActionResult> Index()
{
return View(await _context.Customers.ToListAsync());
}
public async Task<IActionResult> Details(string id)
{
if (id == null)
{
return NotFound();
}
var customer = await _context.Customers
.Include(c => c.Orders)
.ThenInclude(o => o.OrderDetails)
.ThenInclude(od => od.Product)
.FirstOrDefaultAsync(c => c.CustomerId == id);
var orderDetails = customer?.Orders
.SelectMany(o => o.OrderDetails);
if (orderDetails != null)
{
ViewData["PastOrderedProducts"] = orderDetails
.GroupBy(od => od.Product)
.Select(g => new PastOrderedProducts
{
ProductId = g.Key.ProductId,
ProductName = g.Key.ProductName,
Quantity = g.Sum(g => g.Quantity)
}).ToList();
}
if (customer == null)
{
return NotFound();
}
return View(customer);
}
}
public class PastOrderedProducts
{
public int ProductId { get; set; }
public string? ProductName { get; set; }
public int Quantity { get; set; }
}
}