LINQ to Object - 如何实现子元素的动态 SELECT 投影

LINQ to Object - How to implement dynamic SELECT projection of sub elements

我有几个类的业务逻辑:

public class Client {
    public string Code { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public string Account { get; set; } = string.Empty;
    public Total Total { get; set; } = new Total();
    public List<Month> Months { get; set; } = new List<Month>();
}

public class Month {
    public int Number { get; set; } = 0;
    public string Name { get; set; } = string.Empty;
    public DateTime Start { get; set; } = new DateTime();
    public DateTime End { get; set; } = new DateTime();
    public Total Summary { get; set; } = new Total();
}

public class Total {
    public int Count { get; set; } = 0;
    public decimal Sum { get; set; } = 0.0m;
}

实例如下:

List<Client> clients = new List<Client>() {
    new Client {
        Code = "7002.70020604",
        Status = "Active",
        Account = "7002.915940702810005800001093",
        Total = new Total {
            Count = 9,
            Sum = 172536.45m
        },
        Months = new List<Month>() {
            new Month {
                Number = 0,
                Name = "January",
                Start = new DateTime(2021, 1, 1, 0, 0, 0),
                End = new DateTime(2021, 1, 31, 23, 59, 59),
                Summary = new Total {
                    Count = 6,
                    Sum = 17494.50m
                }
            },
            new Month {
                Number = 1,
                Name = "February",
                Start = new DateTime(2021, 2, 1, 0, 0, 0),
                End = new DateTime(2021, 2, 28, 23, 59, 59),
                Summary = new Total {
                    Count = 3,
                    Sum = 155041.95m
                }
            },
            new Month {
                Number = 2,
                Name = "March",
                Start = new DateTime(2021, 3, 1, 0, 0, 0),
                End = new DateTime(2021, 3, 31, 23, 59, 59),
                Summary = new Total {
                    Count = 0,
                    Sum = 0.0m
                }
            }   
        }
    },
    new Client {
        Code = "7002.70020604",
        Status = "Active",
        Account = "7002.800540702810205800001093",
        Total = new Total {
            Count = 4,
            Sum = 16711.21m
        },
        Months = new List<Month>() {
            new Month {
                Number = 0,
                Name = "January",
                Start = new DateTime(2021, 1, 1, 0, 0, 0),
                End = new DateTime(2021, 1, 31, 23, 59, 59),
                Summary = new Total {
                    Count = 0,
                    Sum = 0.0m
                }
            },
            new Month {
                Number = 1,
                Name = "February",
                Start = new DateTime(2021, 2, 1, 0, 0, 0),
                End = new DateTime(2021, 2, 28, 23, 59, 59),
                Summary = new Total {
                    Count = 0,
                    Sum = 0.0m
                }
            },
            new Month {
                Number = 2,
                Name = "March",
                Start = new DateTime(2021, 3, 1, 0, 0, 0),
                End = new DateTime(2021, 3, 31, 23, 59, 59),
                Summary = new Total {
                    Count = 4,
                    Sum = 16711.21m
                }
            }   
        }
    }
};

我正在尝试像这样安排视图的聚合数据:

+---------------+--------+------------------+-------------------+------------------+-------------------+
|      Code     | Status |      January     |      February     |       March      |       Total       |
|               |        +-------+----------+-------+-----------+-------+----------+-------+-----------+
|               |        | Count |    Sum   | Count |    Sum    | Count |    Sum   | Count |    Sum    |
+---------------+--------+-------+----------+-------+-----------+-------+----------+-------+-----------+
| 7002.70020604 | Active |   6   | 17494.50 |   3   | 155041.95 |   4   | 16711.21 |   13  | 189247.66 |
+---------------+--------+-------+----------+-------+-----------+-------+----------+-------+-----------+

像这样使用投影:

clients
    .GroupBy(x => x.Code)
    .Select(y => new {
        Code = y.First().Code,
        Status = y.First().Status,
        Account = y.First().Account,
        Total = new {
            Count = y.Sum(z => z.Total.Count),
            Sum = y.Sum(z => z.Total.Sum)
        },
        Months = new {
            /*
                ?
            */
        }
    });

但是我无法按月投影数据。假设日期范围(月份)可以不止这个例子。请帮忙!

完全互动 code listing 在 dotnetfiddle

您可以使用 SelectManyy 中获取月份,然后按月份分组,就像按代码分组一样:

//...

Months = y
    .SelectMany(client => client.Months)
    .GroupBy(month => month.Name, (_, months) => new {
        Number = months.First().Number,
        Name = months.First().Name,
        Start = months.First().Start,
        End = months.First().End,
        Summary = new {
            Count = months.Sum(z => z.Summary.Count),
            Sum = months.Sum(z => z.Summary.Sum)
        }
    }).ToList()

//...

话虽这么说,但我不建议在每个函数中多次使用 y.First()months.First(),因为每次使用时都会进行枚举。以下通常应该有更好的性能:

(_, months) => {
    var month = months.First();
    return new {
        Number = month.Number,
        Name = month.Name,
        Start = month.Start,
        End = month.End,
        Summary = new {
            Count = months.Sum(z => z.Summary.Count),
            Sum = months.Sum(z => z.Summary.Sum)
        }
    }
}

这也不理想,因为我们仍在此处进行 3 次枚举(.First() 中的 1 次枚举和每个 .Sum(...) 中的 1 次枚举)。

更好的方法是使用 Aggregate 函数,它只会进行一次枚举:

(_, months) => months
    .Aggregate((res, nextVal) => new Month {
        Number = nextVal.Number,
        Name = nextVal.Name,
        Start = nextVal.Start,
        End = nextVal.End,
        Summary = new Total {
            Count = res.Summary.Count + nextVal.Summary.Count,
            Sum = res.Summary.Sum + nextVal.Summary.Sum
        }
    })

此 LINQ 查询应该为可视化准备数据:

clients
    .GroupBy(x => new {x.Code, x.Status})
    .Select(g => new 
    {
        Code = g.Key
        MonthsSummary = g.SelectMany(x => x.Months)
            .OrderBy(x => x.Start)
            .GroupBy(x => new {x.Start, x.Name})
            .Select(gm => new 
            {
                gm.Key.Name,
                Count = gm.Sum(x => x.Summary.Count),
                Sum = gm.Sum(x => x.Summary.Sum),
            })
            .ToList()
    });