PAYATHON 2026

如何使用 Linq2Sql 按月份、Type 和 Description 分组

AI 小顾

结论

在 group by 的复合键中加入 Date.Year、Date.Month、Type 和 Description,然后对每组的 Amount 调用 Sum。

年份不能省略,否则 2011 年 11 月和 2012 年 11 月的数据会被合并。月份名称只用于展示,最好先在数据库中完成汇总,再进行转换,不要直接写进 Linq to SQL 查询。

定义返回模型

公开方法不能直接返回匿名类型,因此需要先定义一个用于网格绑定的模型:

public sealed class BillingHistoryItem
{
    public int UserID { get; set; }
    public int Year { get; set; }
    public int Month { get; set; }
    public string MonthName { get; set; }
    public string Type { get; set; }
    public string Description { get; set; }
    public decimal Amount { get; set; }
}

如果数据库映射中的 Type 不是 string,请将该属性改为实际的映射类型,例如 char。

Linq to SQL 查询

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

public static IEnumerable<BillingHistoryItem> GetBillingHistory(
    int userId,
    DateTime startDate,
    DateTime endDate)
{
    using (SBMData2.SBMDataContext db = new SBMData2.SBMDataContext())
    {
        var groupedQuery =
            from b in db.Billings
            where b.UserID == userId
               && b.Date >= startDate
               && b.Date < endDate
            group b by new
            {
                b.UserID,
                Year = b.Date.Year,
                Month = b.Date.Month,
                b.Type,
                b.Description
            }
            into billingGroup
            orderby billingGroup.Key.Year descending,
                    billingGroup.Key.Month descending,
                    billingGroup.Key.Type,
                    billingGroup.Key.Description
            select new
            {
                billingGroup.Key.UserID,
                billingGroup.Key.Year,
                billingGroup.Key.Month,
                billingGroup.Key.Type,
                billingGroup.Key.Description,
                Amount = billingGroup.Sum(x => x.Amount)
            };

        return groupedQuery
            .AsEnumerable()
            .Select(x => new BillingHistoryItem
            {
                UserID = x.UserID,
                Year = x.Year,
                Month = x.Month,
                MonthName = CultureInfo
                    .GetCultureInfo("en-US")
                    .DateTimeFormat
                    .GetMonthName(x.Month),
                Type = x.Type,
                Description = x.Description,
                Amount = x.Amount
            })
            .ToList();
    }
}

这里的分组键相当于:

UserID + Year + Month + Type + Description

同一用户在同一年、同一个月内,只要 Type 和 Description 相同,相关记录就会汇总成一行。

AsEnumerable() 之前的筛选、分组、排序和求和由数据库执行。转到内存后,再把月份数字转换为 November 这样的名称,因为 Linq to SQL 通常无法将 DateTimeFormat.GetMonthName() 翻译成 SQL。

如果界面需要显示中文月份,可以改为:

CultureInfo
    .GetCultureInfo("zh-CN")
    .DateTimeFormat
    .GetMonthName(x.Month)

也可以直接显示年月,让不同年份的同名月份更容易区分:

string.Format("{0:D4}-{1:D2}", x.Year, x.Month)

页面绑定

页面的绑定方式不需要调整:

protected void Page_Load(object sender, EventArgs e)
{
    int userID = foo.Data.User.GetIDFromUserName(
        HttpContext.Current.User.Identity.Name);

    DateTime endDate = DateTime.Now;
    DateTime startDate = endDate.AddMonths(-3);

    RadGrid2.DataSource = BusinessLogic.GetBillingHistory(
        userID,
        startDate,
        endDate);

    RadGrid2.DataBind();
}

日期范围需要注意

下面的条件使用左闭右开区间:

b.Date >= startDate && b.Date < endDate

与 <= endDate 相比,这种写法更适合 endDate 表示下一周期起点的情况,可以避免重复计算边界上的记录。

DateTime.Now.AddMonths(-3) 得到的是向前推三个月的滚动区间,不一定包含三个完整的日历月。如果需要查询本月和之前两个完整月份,可以明确计算每月月初:

DateTime currentMonthStart =
    new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);

DateTime startDate = currentMonthStart.AddMonths(-2);
DateTime endDate = currentMonthStart.AddMonths(1);

金额显示

查询只负责计算金额。美元符号和负数括号等格式应由网格展示层处理,例如:

amount.ToString("C")

付款是否显示为负数,要看数据库中的 Amount 是否已经按负值保存。如果付款记录存储的是正数,需要按照业务规则在投影中转换符号。现有信息无法确认 Type == "C" 是否一定代表付款,因此不应在规则尚未明确时直接修改金额。

备注:内容仅供参考。