今天踩了個低級錯誤的雷,分享一下。

網頁上有個欄位輸入預期執行日期時間,後端檢核時需比對不得早於現在時間,前端傳入的 JSON 已轉為 UTC,例如在 7/25 選明天(7/26), 會得到 "2019-07-25T16:00:00Z",用 JsonConvert.Deserialize<DateTime>(jsonString) 轉成 C# DataTime 物件後,我很直覺的用它跟 DateTime.Now 比較,上午中午測試還沒出錯,下午 4:00 後再測,竟出現「明天比現在早」的荒謬結果。

用範例程式重現 Bug 如下:

static void Test1()
{
    var tomorrowUtcJson = 
        JsonConvert.SerializeObject(DateTime.Today.AddDays(1).ToUniversalTime());
    Console.WriteLine(tomorrowUtcJson);
    var tomorrow = JsonConvert.DeserializeObject<DateTime>(tomorrowUtcJson);
    //台北時間下午四點之後執行會冒出明天小於今天的Bug
    Console.WriteLine("Tomorrow < Now = {0}", tomorrow < DateTime.Now);
}

這段程式在台北時間 16:00 之後跑會看到 Tomorrow < Now = True 的奇妙結果。

問題很簡單,DateTime 在比大小時是不管時區的。參考官方文件

The LessThan operator determines the relationship between two DateTime values by comparing their number of ticks. Before comparing DateTime objects, make sure that the objects represent times in the same time zone.

明天時間換算 UTC 減 8 小時是下午 4 點,因此下午 4 點後便會出現明天比現在時間還早的奇觀。

要解決很簡單,方法有二,第一個是一律轉成 UTC 或本地時間再比較,第二是將 DateTime 轉成 DateTimeOffset,依據官方文件

In performing the comparison, the method converts both the first and the second parameters to Coordinated Universal Time (UTC) before it performs the comparison.

static void Test2()
{
    var tomorrow = DateTime.Today.AddDays(1).ToUniversalTime();
    //方法一,一律轉UTC再比
    Console.WriteLine("Tomorrow < Now = {0}", tomorrow.ToUniversalTime() < DateTime.Now.ToUniversalTime());
    //方法二,轉成DateTimeOffset,DateTimeOffset內部會轉UTC再比較
    Console.WriteLine("Tomorrow < Now = {0}", new DateTimeOffset(tomorrow) < new DateTimeOffset(DateTime.Now));
}

同場加映:DateTime, DateTimeOffset, Timespan, TimeZoneInfo 該怎麼選?

The experience of incorrectly comparring DateTime values with different timezones. DateTimeOffset is better type to use.


Comments

# by Anonymous

版大可以幫忙研究 DateTimeOffset 和 LINQ / Entity Framework 的關係嗎? 之前在 EF 上用過很驚豔,但想從版大中再偷學一些技術~

# by Jeffrey

to Anonymous, 先前倒沒發現過,可以分享驚豔點何在嗎?

# by Anonymous

SQL Server 也有 DateTimeOffset 程式撰寫時可以很腦殘的來個 DateTimeOffset.Now Entity Framework 在 Model 中又可以做到 [NotMapped] 的自訂格式輸出 Getter/Setter 也可以做到轉換,包含時區、對映 DateTime、時間增減 很難在其他語言中找到替代方案

Post a comment