MessagePack 筆記 (2)

使用 MessagePack 處理 DateTime 型別時有一些細節需要注意,來聊一下是怎麼回事。

緣起

在微軟的文件「在適用於 ASP.NET Core 的 SignalR 中使用 MessagePack 中樞通訊協定」中有提到「序列化/還原序列化時,不會保留 DateTime.Kind」,這感覺上會是個小小的麻煩,所以還是寫個筆記,以免將來遇到又忘了是怎麼回事。

測試

先來進行一個小小的測試範例,來源日期的形式分別是 (1)  DateTimeKind.Unspecified (2) DateTimeKind.Local (3) DateTimeKind.Utc,看看它們被 MessagePack 處理後會變成甚麼樣子。

 internal class Program
 {
     static void Main(string[] args)
     {
         // source DateTimeKind : Unspecified
         DateTime unspecifiedDate = new DateTime(1985, 3, 4);           
         DateTime unspecifiedDateDeserialized = SerializeThenDeserialize(unspecifiedDate);
         DisplayResult(unspecifiedDate, unspecifiedDateDeserialized);


         // source DateTimeKind : Local
         DateTime localDate = new DateTime(1985, 3, 5, 0, 0, 0, DateTimeKind.Local);
         DateTime localDateDeserialized = SerializeThenDeserialize(localDate);
         DisplayResult(localDate, localDateDeserialized);

         // source DateTimeKind : Utc
         DateTime utcDate = new DateTime(1985, 3, 6, 0, 0, 0, DateTimeKind.Utc);
         DateTime utcDateDeserialized = SerializeThenDeserialize(utcDate);
         DisplayResult(utcDate, utcDateDeserialized);
     }

     static DateTime SerializeThenDeserialize(DateTime source)
     {
         byte[] bytes = MessagePackSerializer.Serialize(source);
         return MessagePackSerializer.Deserialize<DateTime>(bytes);
     }

     static void DisplayResult(DateTime source, DateTime deserialized)
     {
         Console.WriteLine($"Source Date: ({source}:{source.Kind}), Deserialized Date: ({deserialized}:{deserialized.Kind})");
     }
 }
測試的結果為:
Source Date: (1985/3/4 上午 12:00:00:Unspecified), Deserialized Date: (1985/3/4 上午 12:00:00:Utc)
Source Date: (1985/3/5 上午 12:00:00:Local), Deserialized Date: (1985/3/4 下午 04:00:00:Utc)
Source Date: (1985/3/6 上午 12:00:00:Utc), Deserialized Date: (1985/3/6 上午 12:00:00:Utc)

結果很明顯,都會變成 UTC Time。

解決方案 (1)

第一個解決方案很單純,就是所有資料模型與資料庫的日期時間都採用 UTC,本地時間的部分靠 ViewModel 或 View 處理,對新專案來說這大概是最不傷腦筋的做法了。

解決方案 (2)

有時逼不得已,可能非得在資料模型端處理這問題就得耍點小手段了。很常見的一個情形是在原有程式碼改用 MessagePack,而且很不幸地在程式碼的各處充滿著會被當成 Local Time 使用的 Unspecified Time。

首先搞一個轉換用的擴充方法把所有時間都變成 Local Time:

 public static class DateTimeExtensions
 {
     public static DateTime ConvertToLocalTime(this DateTime dateTime)
     {
         if (dateTime.Kind == DateTimeKind.Utc)
         {
             return dateTime.ToLocalTime();
         }
         else if (dateTime.Kind == DateTimeKind.Unspecified)
         {
             return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
         }
         else
         {
             return dateTime;
         }
     }
 }
  1. 遇到 UTC ,呼叫 ToLocalTime,比方 UTC 的 1985/3/4 16:00:00,轉換成 Local (例如: 台灣的 UTC+8) 就會變成 1985/3/5 00:00:00
  2. 遇到 Unspecified,表示這個時間應該是 Local Time,所以呼叫 DateTime.SpecifyKind,也就是 Unspecified 1985/3/4 16:00:00 會變成 Local 1985/3/4 16:00:00 。

資料模型的設計則是在遇到 DateTime 時需要在 setter 上呼叫上述的擴充方法:

 [MessagePackObject]
 public class Person
 {
     [Key(0)]
     public string Name { get; set; }

     private DateTime _birthDay;
     [Key(1)]
     public DateTime BirthDay
     {
         get => _birthDay;
         set => _birthDay = value.ConvertToLocalTime();
     }
 }

接著來試試看:

 internal class Program
 {
     static void Main(string[] args)
     {
         Person person = new Person
         {
             Name = "Bill",
             BirthDay = new DateTime(1985, 3, 4, 0, 0, 0)
         };

         Console.WriteLine("from Unspecified");
         SerializeAndDeserializePerson(person);
         Console.WriteLine();
         person.BirthDay = new DateTime(1985, 3, 5, 0, 0, 0, DateTimeKind.Utc);
         Console.WriteLine("from Utc");
         SerializeAndDeserializePerson(person);
         Console.WriteLine();
         person.BirthDay = new DateTime(1985, 3, 6, 0, 0, 0, DateTimeKind.Local);
         Console.WriteLine("from Local");
         SerializeAndDeserializePerson(person);
     }

     private static void SerializeAndDeserializePerson(Person person)
     {
         var bytes = MessagePackSerializer.Serialize(person);
         var personDeserialized = MessagePackSerializer.Deserialize<Person>(bytes);
         DisplayResult("person", person.BirthDay);
         DisplayResult("personDeserialized", personDeserialized.BirthDay);
     }

     private static void DisplayResult(string prefix, DateTime value)
     {
         Console.WriteLine($"{prefix} : {value}, DateTimeKind: {value.Kind}");
     }
 }
結果顯示:
from Unspecified
person : 1985/3/4 上午 12:00:00, DateTimeKind: Local
personDeserialized : 1985/3/4 上午 12:00:00, DateTimeKind: Local

from Utc
person : 1985/3/5 上午 08:00:00, DateTimeKind: Local
personDeserialized : 1985/3/5 上午 08:00:00, DateTimeKind: Local

from Local
person : 1985/3/6 上午 12:00:00, DateTimeKind: Local
personDeserialized : 1985/3/6 上午 12:00:00, DateTimeKind: Local

當然可能還有其他情境與方式,大致上就是用點想像力去解決。這個範例程式在此