Protocol Buffers by Google are a great mechanism for serializing (and deserializing) structured data in a very fast and efficient way. Protobuf-net is Marc Gravell’s port of Protocol Buffers for the .NET ecosystem.
While being very efficient, protobuf-net has a big issue when deserializing .NET’s DateTime
s. Behind the scenes DateTime
s are converted into Unix-Time which is a count (of ticks in this case) starting from the Unix Epoch (1970/01/01 UTC). When deserializing back to .NET protobuf-net adds that count to a DateTime
representing the Epoch-Time resulting in the correct DateTime
value. The issue with this process is that it loses the DateTime
’s original DateTimeKind
.
DateTimeKind
is an enum telling whether the DateTime
’s value represents a local time, UTC time or unspecified. That value isn’t serialized by protobuf-net so all DateTime
s, be they local time or UTC, are deserialized as DateTimeKind.Unspecified
.
DateTimeKind.Unspecified
values have a behavior that I initially found surprising but later realized is the best possible option. Let’s assume you’re in Hawaii (because where else would you want to be?) and your time zone is UTC-10:00. If you have a DateTime
value with DateTimeKind.Unspecified
and you call ToLocalTime
the method assumes the value is in UTC and “corrects” it, so 11:00 becomes 01:00. If however you call ToUniversalTime
on that value the method now assumes it’s in local time and “corrects” it so 11:00 becomes 21:00. So the same value is treated as local while adjusting to universal and universal when adjusting to local. Let’s see that in code:
static void Main()
{
var dolly = new Sheep { DateOfBirth = new DateTime(1966, 07, 05, 11, 0, 0, DateTimeKind.Utc) };
Console.WriteLine(dolly.DateOfBirth.ToString("HH:mm:ss K")); // "11:00:00 Z" (Z means UTC)
dolly = Serializer.DeepClone(dolly); // Serialize and deserialize using protobuf-net
Console.WriteLine(dolly.DateOfBirth.ToString("HH:mm:ss K")); // "11:00:00" (no Z means unspecified)
Console.WriteLine(dolly.DateOfBirth.ToLocalTime().ToString("HH:mm:ss K")); // "01:00:00 -10:00" (Hawaii timezone)
Console.WriteLine(dolly.DateOfBirth.ToUniversalTime().ToString("HH:mm:ss K")); // "21:00:00 Z"
}
[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
class Sheep
{
public DateTime DateOfBirth { get; set; }
}
This can get extremely problematic especially if you, like me, depend upon some library that uses ToUniversalTime
or ToLocalTime
. For me that library was the .NET’s MongoDB Driver that stores all DateTime
s in MongoDB in UTC. Using these 2 libraries together is impossible as DateTime
values would keep changing value infinitely.
I have posted on the protobuf-net repository on github explaining this and managed to convince him to fix this problem (which he did with this commit). However, this fix was made 5 months prior to me writing this post and there’s still isn’t a new release including the fix (the latest stable release is from 2013/09/30).
But don’t fear… I do have a workaround you can use for the meantime. Protobuf-net uses a DateTime
value representing the Unix Epoch (1970/01/01) to create all the DateTime
s by adding the relevant delta to the epoch value. Since creating a new DateTime
from an existing DateTime
preserves the DateTimeKind
, replacing the single epoch value with a UTC one will result with all protobuf-net DateTime
values having DateTimeKind.UTC
. We can do that by using reflection and replacing the epoch value with a UTC one:
typeof (BclHelpers).
GetField("EpochOrigin", BindingFlags.NonPublic | BindingFlags.Static).
SetValue(null, new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc));
var dolly = new Sheep { DateOfBirth = new DateTime(1966, 07, 05, 11, 0, 0, DateTimeKind.Utc) };
Console.WriteLine(dolly.DateOfBirth.ToString("HH:mm:ss K")); // "11:00:00 Z" (Z means UTC)
dolly = Serializer.DeepClone(dolly); // Serialize and deserialize using protobuf-net
Console.WriteLine(dolly.DateOfBirth.ToString("HH:mm:ss K")); // "11:00:00 Z"
I admit it’s not pretty, but until Marc releases a new version, it’s preferable to building your own protobuf-net.