使用 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;
}
}
}
- 遇到 UTC ,呼叫 ToLocalTime,比方 UTC 的 1985/3/4 16:00:00,轉換成 Local (例如: 台灣的 UTC+8) 就會變成 1985/3/5 00:00:00
- 遇到 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
當然可能還有其他情境與方式,大致上就是用點想像力去解決。這個範例程式在此。