如何使用Linq实现三层嵌套GroupBy?求最优实现方案
嘿,我来帮你搞定这个三层GroupBy的需求!从你给出的扁平数据集字段来看,核心应该是把数据按「MasterPlanType → MasterPlan → 明细/聚合数据」这样的三层嵌套结构来分组,下面我一步步给你讲实现方式和最优实践。
首先,我们可以用嵌套的GroupBy来实现层级分组。因为分组时需要用复合键(比如MasterPlanTypeID+MasterPlanTypeName,ID是唯一标识,Name用于展示),匿名类型在这里很方便——它默认按值相等比较,完美适配分组需求。
假设你的扁平数据模型叫MasterPlanFlatItem,原始数据是IEnumerable<MasterPlanFlatItem> flatData,代码如下:
var groupedData = flatData // 第一层:按MasterPlanType分组,键包含ID和名称 .GroupBy(item => new { item.MasterPlanTypeID, item.MasterPlanTypeName }) // 遍历每个Type分组,进行第二层分组 .Select(typeGroup => new { TypeID = typeGroup.Key.MasterPlanTypeID, TypeName = typeGroup.Key.MasterPlanTypeName, // 第二层:按MasterPlan分组 MasterPlans = typeGroup .GroupBy(planItem => new { planItem.MasterPlanID, planItem.MasterPlanName }) // 遍历每个Plan分组,处理第三层(这里以聚合Duration为例,也可以保留明细) .Select(planGroup => new { PlanID = planGroup.Key.MasterPlanID, PlanName = planGroup.Key.MasterPlanName, // 聚合Duration字段 MinDuration = planGroup.Min(p => p.MasterPlanMinDuration), MaxDuration = planGroup.Max(p => p.MasterPlanMaxDuration), // 如果需要保留该Plan下的所有原始明细,添加这个属性: // Details = planGroup.ToList() }) .ToList() }) .ToList();
这段代码会把扁平数据转换成三层嵌套结构:每个类型下包含多个计划,每个计划下包含聚合后的Duration数据(或原始明细)。
如果数据量较大,或者需要复用分组逻辑,可以优化以下几点:
1. 使用强类型分组键(C# 9+推荐用Record)
匿名类型虽然方便,但如果需要在多个地方复用分组逻辑,或者要明确分组键的含义,推荐用Record类型(C# 9及以上支持)——它默认实现了值相等比较,和匿名类型行为一致,但更具可读性和复用性:
// 定义强类型分组键 public record MasterPlanTypeKey(string MasterPlanTypeID, string MasterPlanTypeName); public record MasterPlanKey(string MasterPlanID, string MasterPlanName); // 分组时使用强类型键 var groupedData = flatData .GroupBy(item => new MasterPlanTypeKey(item.MasterPlanTypeID, item.MasterPlanTypeName)) .Select(typeGroup => new { TypeID = typeGroup.Key.MasterPlanTypeID, TypeName = typeGroup.Key.MasterPlanTypeName, MasterPlans = typeGroup .GroupBy(planItem => new MasterPlanKey(planItem.MasterPlanID, planItem.MasterPlanName)) .Select(planGroup => new { PlanID = planGroup.Key.MasterPlanID, PlanName = planGroup.Key.MasterPlanName, MinDuration = planGroup.Min(p => p.MasterPlanMinDuration), MaxDuration = planGroup.Max(p => p.MasterPlanMaxDuration) }) .ToList() }) .ToList();
2. 提前过滤不必要的数据
在分组前,先用Where过滤掉不需要的条目,减少分组处理的数据量,提升性能:
var filteredData = flatData.Where(item => !string.IsNullOrEmpty(item.MasterPlanID)); // 再对filteredData进行分组操作
3. 避免重复计算聚合值
如果在分组后需要多次访问聚合结果(比如MinDuration),提前计算并存储,不要每次都调用Min()/Max():
.Select(planGroup => { var minDur = planGroup.Min(p => p.MasterPlanMinDuration); var maxDur = planGroup.Max(p => p.MasterPlanMaxDuration); return new { PlanID = planGroup.Key.MasterPlanID, PlanName = planGroup.Key.MasterPlanName, MinDuration = minDur, MaxDuration = maxDur, // 比如需要计算Duration范围,直接用提前算好的值 DurationRange = $"{minDur}-{maxDur}" }; })
4. 内存分组的性能注意事项
因为你的数据是LinqToExcel读取的内存数据,不用担心ORM的SQL转换问题。内存中GroupBy的性能主要取决于数据量,上面的实现已经是高效的——Linq的GroupBy内部会用哈希表来分组,时间复杂度是O(n)。
如果你的三层需求不是「Type→Plan→聚合/明细」,而是Type→Plan→另一个维度(比如你没写完的S...字段,比如Stage),只需要在第二层分组后再加一层GroupBy即可。比如按计划的阶段分组:
var groupedData = flatData .GroupBy(item => new { item.MasterPlanTypeID, item.MasterPlanTypeName }) .Select(typeGroup => new { TypeID = typeGroup.Key.MasterPlanTypeID, TypeName = typeGroup.Key.MasterPlanTypeName, MasterPlans = typeGroup .GroupBy(planItem => new { planItem.MasterPlanID, planItem.MasterPlanName }) .Select(planGroup => new { PlanID = planGroup.Key.MasterPlanID, PlanName = planGroup.Key.MasterPlanName, // 第三层:按Stage分组(假设字段是StageID) StageGroups = planGroup .GroupBy(stageItem => stageItem.StageID) .Select(stageGroup => new { StageID = stageGroup.Key, StageItems = stageGroup.ToList() }) .ToList() }) .ToList() }) .ToList();
内容的提问来源于stack exchange,提问作者Silagy

