DateTime.Today 避免時間部分干擾邊界判斷。DateTime 型別: 由呼叫端決定 .ToString("yyyy-MM-dd") 或傳給參數化查詢,避免字串拼接 SQL 風險。
public static class DateRangePresets
{
// 基準日 (可注入測試)
private static DateTime Today => DateTime.Today;
// ========== 週範圍 (週日為週起始,符合 .NET DayOfWeek) ==========
public static (DateTime Start, DateTime End) ThisWeek()
{
var start = Today.AddDays(-(int)Today.DayOfWeek);
return (start, start.AddDays(6));
}
public static (DateTime Start, DateTime End) LastWeek() => ThisWeek().AddDays(-7);
public static (DateTime Start, DateTime End) NextWeek() => ThisWeek().AddDays(7);
// 擴充方法簡化寫法
private static (DateTime Start, DateTime End) AddDays(this (DateTime, DateTime) r, int days)
=> (r.Item1.AddDays(days), r.Item2.AddDays(days));
// ========== 月範圍 ==========
public static (DateTime Start, DateTime End) ThisMonth()
{
var start = new DateTime(Today.Year, Today.Month, 1);
return (start, start.AddMonths(1).AddDays(-1));
}
public static (DateTime Start, DateTime End) LastMonth() => ThisMonth().AddMonths(-1);
public static (DateTime Start, DateTime End) NextMonth() => ThisMonth().AddMonths(1);
private static (DateTime Start, DateTime End) AddMonths(this (DateTime, DateTime) r, int months)
=> (r.Item1.AddMonths(months), r.Item2.AddMonths(months));
// ========== 季範圍 ==========
public static (DateTime Start, DateTime End) ThisQuarter()
{
int q = (Today.Month - 1) / 3; // 0,1,2,3
var start = new DateTime(Today.Year, q * 3 + 1, 1);
return (start, start.AddMonths(3).AddDays(-1));
}
public static (DateTime Start, DateTime End) LastQuarter() => ThisQuarter().AddMonths(-3);
public static (DateTime Start, DateTime End) NextQuarter() => ThisQuarter().AddMonths(3);
private static (DateTime Start, DateTime End) AddMonths(this (DateTime, DateTime) r, int months)
=> (r.Item1.AddMonths(months), r.Item2.AddMonths(months));
// ========== 年範圍 ==========
public static (DateTime Start, DateTime End) ThisYear()
=> (new DateTime(Today.Year, 1, 1), new DateTime(Today.Year, 12, 31));
public static (DateTime Start, DateTime End) LastYear() => ThisYear().AddYears(-1);
public static (DateTime Start, DateTime End) NextYear() => ThisYear().AddYears(1);
private static (DateTime Start, DateTime End) AddYears(this (DateTime, DateTime) r, int years)
=> (r.Item1.AddYears(years), r.Item2.AddYears(years));
// ========== 相對天數 ==========
public static (DateTime Start, DateTime End) LastNDays(int n)
=> (Today.AddDays(-n), Today);
public static (DateTime Start, DateTime End) NextNDays(int n)
=> (Today, Today.AddDays(n));
}
public class ReportController : Controller
{
public IActionResult Sales(string range = "thisMonth")
{
(DateTime start, DateTime end) = range switch
{
"today" => (DateTime.Today, DateTime.Today),
"yesterday" => (DateTime.Today.AddDays(-1), DateTime.Today.AddDays(-1)),
"thisWeek" => DateRangePresets.ThisWeek(),
"lastWeek" => DateRangePresets.LastWeek(),
"thisMonth" => DateRangePresets.ThisMonth(),
"lastMonth" => DateRangePresets.LastMonth(),
"thisQuarter"=> DateRangePresets.ThisQuarter(),
"lastQuarter"=> DateRangePresets.LastQuarter(),
"thisYear" => DateRangePresets.ThisYear(),
"lastYear" => DateRangePresets.LastYear(),
"last7d" => DateRangePresets.LastNDays(7),
"last30d" => DateRangePresets.LastNDays(30),
_ => DateRangePresets.ThisMonth()
};
// 參數化查詢,完全避免 SQL 注入與格式問題
var data = _db.Sales
.Where(s => s.Date >= start && s.Date <= end)
.GroupBy(s => s.ProductId)
.Select(g => new { Product = g.Key, Total = g.Sum(x => x.Amount) })
.ToList();
ViewBag.Range = $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}";
return View(data);
}
}
若需直接產生字串 (如檔名、URL 參數),可用格式化捷徑:
// 月首日
string monthStartStr = DateTime.Today.ToString("yyyy-MM-01"); // "2024-03-01"
// 月末日
string monthEndStr = DateTime.Parse(monthStartStr).AddMonths(1).AddDays(-1).ToString("yyyy-MM-dd");
// 年首日
string yearStartStr = DateTime.Today.ToString("yyyy-01-01");
// 季首日 (需計算)
int q = (DateTime.Today.Month - 1) / 3;
string quarterStartStr = new DateTime(DateTime.Today.Year, q * 3 + 1, 1).ToString("yyyy-MM-dd");
| 錯誤做法 | 問題 | 正確做法 |
|---|---|---|
strDate1 > strDate2 | 字串字典序非時間序 | DateTime.Parse(str1) > DateTime.Parse(str2) |
new DateTime(y, m, d+3) | 月末溢位拋例外 | date.AddDays(3) |
| 忽略時區比較 | 跨時區用戶見不同結果 | 統一用 UTC 或 DateOnly |
DateTime.Compare(d1, d2) | 含時間部分干擾 | d1.Date.CompareTo(d2.Date) |
public bool IsRecent(DateTime createTime)
{
// 僅比較日期部分,忽略建立時間的時分秒
return DateTime.Today <= createTime.Date.AddDays(3);
}
// LINQ to Entities 相容寫法 (EF Core 轉譯為 SQL)
var recentLogs = db.Logs.Where(l => l.CreateTime >= DateTime.Today.AddDays(-3)).ToList();
DateOnly today = DateOnly.FromDateTime(DateTime.Today);
DateOnly weekStart = today.AddDays(-(int)today.DayOfWeek);
DateOnly monthStart = new DateOnly(today.Year, today.Month, 1);
DateOnly quarterStart = new DateOnly(today.Year, (today.Month - 1) / 3 * 3 + 1, 1);
// 轉回 DateTime 給舊 API
DateTime dt = weekStart.ToDateTime(TimeOnly.MinValue);
將日期範圍邏輯封裝為 靜態工具類 + 擴充方法,配合 參數化查詢 與 前端枚舉值對應,即可實現報表查詢「零硬編碼、零格式錯誤、零時區坑」。記住三句口訣:用 Today 不用 Now、用 AddDays 不用算天數、用 DateOnly 表達純日期。