Linq の GroupBy を使ってグループ分けを行い、グループに属するレコード一覧を取得する際に 1, 2, 3 ... という連番を振る方法を書きます。

上の画像は ASP.NET Core MVC アプリのもので、Northwind サンプルデータベースの Products テーブルのレコードを Supplier でグループ分けし、各グループに含まれるレコードの ProductName と UnitPrice を取得するとともに ProductId 順に 1, 2, 3 ... という Index (連番) を振って表示したものです。
先の記事「Entity Framework で ROW_NUMBER」で、Linq to Objects で SQL 文の ROW_NUMBER を使った場合と同様な連番を振る方法を紹介しました。それを GroupBy でグループ分けした各グループ内で行うものです。
Linq の GroupBy メソッドを使ってグループ分けすると IEnumerable<IGrouping<TKey, TElement>> オブジェクトが得られます。IGrouping<TKey, TElement> オブジェクトは共通のキー TKey を持つ TElement のコレクションになります。
Linq の Select メソッドを使って IGrouping<TKey, TElement> オブジェクトを反復処理する際に TElement にアクセスして必要な値を取得できますが、それにオーバーロードの一つである Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,Int32,TResult>) を使うと、同時に 0 番から始まる連番の index を取得することができます。
上の画像の ASP.NET Core MVC アプリのコードを下に載せておきます。使用したコンテキストクラスとエンティティクラスは先の記事「Linq の GroupBy と Aggregate」のものと同じです。上に述べた連番を振る具体例は下のコードの最後の方の Controller / Action Method のコードを見てください。
Model
namespace MvcNet7App.Models
{
public class ProductDTO
{
public int ProductId { get; set; }
public string ProductName { get; set; } = null!;
public string Supplier { get; set; } = null!;
public decimal? UnitPrice { get; set; }
public int Index { get; set; } // 連番用
}
}
View
@model Dictionary<string, IEnumerable<ProductDTO>>
@{
ViewData["Title"] = "GroupBy";
}
@foreach (var item in Model)
{
<h4>Supplier: @Html.DisplayFor(m => item.Value.First().Supplier)</h4>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(m => item.Value.First().ProductId)
</th>
<th>
@Html.DisplayNameFor(m => item.Value.First().Index)
</th>
<th>
@Html.DisplayNameFor(m => item.Value.First().ProductName)
</th>
<th>
@Html.DisplayNameFor(m => item.Value.First().UnitPrice)
</th>
</tr>
</thead>
<tbody>
@foreach (var product in item.Value)
{
<tr>
<td>
@Html.DisplayFor(m => product.ProductId)
</td>
<td>
@Html.DisplayFor(m => product.Index)
</td>
<td>
@Html.DisplayFor(m => product.ProductName)
</td>
<td>
@Html.DisplayFor(m => product.UnitPrice)
</td>
</tr>
}
</tbody>
</table>
}
Controller / Action Method
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;
using System.ComponentModel.DataAnnotations;
namespace MvcNet7App.Controllers
{
public class ProductsController : Controller
{
private readonly NorthwindContext _context;
public ProductsController(NorthwindContext context)
{
_context = context;
}
public async Task<IActionResult> GroupBy()
{
// いきなり _context.Products.GroupBy(p => p.SupplierId) と
// はできないので一旦 List<Product> 型オブジェクトを作り、
List<Product> productList = await _context.Products
.Include(p => p.Supplier)
.ToListAsync();
// Linq to Objects として GroupBy を適用する
IEnumerable<IGrouping<string, Product>> groups =
productList.GroupBy(p => p.Supplier!.CompanyName)
.OrderBy(g => g.Key);
// グループごとに連番を取得して IEnumerable<ProductDTO> オブ
// ジェクトを作り、Dictionary<string, IEnumerable<ProductDTO>>
// に詰め替えて View にモデルとして渡す
var dic = new Dictionary<string, IEnumerable<ProductDTO>>();
foreach (IGrouping<string, Product> group in groups)
{
var groupDTO = group
.OrderBy(p => p.ProductId)
.Select((p, index) => new ProductDTO
{
Supplier = p.Supplier!.CompanyName,
ProductId = p.ProductId,
Index = index + 1,
ProductName = p.ProductName,
UnitPrice = p.UnitPrice
});
dic.Add(group.Key, groupDTO);
}
return View(dic);
}
}
}