使用 C# 读写 MongoDB 时,DateTime 常常在 Unspecified、Local 和 Utc 之间转换,搞不清楚的话很容易弄错。最近写程序弄错了两次,数据老是重复,刚开始一直找不到问题,浪费了两天时间,坑爹。今天查资料、写测试、找源码,整了两个多小时,终于搞清楚了。
关于 DateTimeKind
// DateTimeKind.Local
DateTime td = DateTime.Today;
// DateTimeKind.Unspecified
DateTime dt1 = new DateTime(1999, 1, 1);
// 输出:dt1:1999-01-01 00:00:00, Ticks:630507456000000000
Console.WriteLine("dt1:{0}, Ticks:{1}", dt1, dt1.Ticks);
DateTime dt2 = new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Local);
// 输出:dt2:1999-01-01 00:00:00, Ticks:630507456000000000
Console.WriteLine("dt2:{0}, Ticks:{1}", dt2, dt2.Ticks);
DateTime dt3 = new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// 输出:dt3:1999-01-01 00:00:00, Ticks:630507456000000000
Console.WriteLine("dt3:{0}, Ticks:{1}", dt3, dt3.Ticks);
Console.WriteLine(dt1 == dt2); // 输出:True
Console.WriteLine(dt1 == dt3); // 输出:True
Console.WriteLine(dt2 == dt3); // 输出:True
查看 DateTime 源代码可知,虽然三个时间的 DateTimeKind 不同,但 Ticks 值是一样的,而 DateTime 的默认比较器只比较了 Ticks 值,没有转换到统一的标准。
关于 DateTimeKind 转换和直接指定的问题参考 System.DateTimeKind 的用法。
mongo-csharp-driver(2.8)实现的时间序列化类
1. DateTimeSerializer
默认无参构造函数最终调用的是私有构造函数
DateTimeSerializer(bool dateOnly, DateTimeKind kind, BsonType representation)
参数值为 False, DateTimeKind.Utc,BsonType.DateTime。
-
序列化
DateTime utcDateTime;
if (_dateOnly)
{
if (value.TimeOfDay != TimeSpan.Zero)
{
throw new BsonSerializationException("TimeOfDay component is not zero.");
}
utcDateTime = DateTime.SpecifyKind(value, DateTimeKind.Utc); // not ToLocalTime
}
else
{
utcDateTime = BsonUtils.ToUniversalTime(value);
}
var millisecondsSinceEpoch = BsonUtils.ToMillisecondsSinceEpoch(utcDateTime);
switch (_representation)
{
case BsonType.DateTime:
bsonWriter.WriteDateTime(millisecondsSinceEpoch);
break;
...
}
可见,无论何时 MongoDB 存储的都是 Utc 时间。而默认情况下,更确切的说是代码时间转换为 Utc 时间后的毫秒级的 Unix 时间刻度。
-
反序列化
switch (bsonType)
{
case BsonType.DateTime:
// use an intermediate BsonDateTime so MinValue and MaxValue are handled correctly
value = new BsonDateTime(bsonReader.ReadDateTime()).ToUniversalTime();
break;
...
}
if (_dateOnly)
{
if (value.TimeOfDay != TimeSpan.Zero)
{
throw new FormatException("TimeOfDay component for DateOnly DateTime value is not zero.");
}
value = DateTime.SpecifyKind(value, _kind); // not ToLocalTime or ToUniversalTime!
}
else
{
switch (_kind)
{
case DateTimeKind.Local:
case DateTimeKind.Unspecified:
value = DateTime.SpecifyKind(BsonUtils.ToLocalTime(value), _kind);
break;
case DateTimeKind.Utc:
value = BsonUtils.ToUniversalTime(value);
break;
}
}
可见,默认情况下,MongoDB 中读取出来的 DateTime 是 Utc 时间。要想获取到本地时间,可以为 DateTime 注册一个自定义的序列化接口。可以是自己实现的序列化类,也可以是内置的 DateTimeSerializer 类 + 构造参数 DateTimeKind.Local
BsonSerializer.RegisterSerializer(typeof(DateTime), new DateTimeSerializer(DateTimeKind.Local));
2. DateTimeOffsetSerializer
再看看传说中的 DateTimeOffset。DateTimeOffset 没有 DateTimeKind 成员,只存储了 Ticks,可以说是完全的 Utc 时间。默认无参构造函数调用的是
DateTimeOffsetSerializer(BsonType representation)
参数值为 BsonType.Array。
-
序列化
switch (_representation)
{
case BsonType.Array:
bsonWriter.WriteStartArray();
bsonWriter.WriteInt64(value.Ticks);
bsonWriter.WriteInt32((int)value.Offset.TotalMinutes);
bsonWriter.WriteEndArray();
break;
...
}
-
反序列化
switch (bsonType)
{
case BsonType.Array:
bsonReader.ReadStartArray();
ticks = bsonReader.ReadInt64();
offset = TimeSpan.FromMinutes(bsonReader.ReadInt32());
bsonReader.ReadEndArray();
return new DateTimeOffset(ticks, offset);
...
}
DateTimeOffsetSerializer 的存取逻辑看起来简单粗暴得多。
其他文档数据库的时间序列化类
其他 BSON 文档数据库对 DateTime 的存取逻辑也类似,如 LiteDB(4.1.4)的序列化类。
1. BsonSerializer
-
序列化
switch (value.Type)
{
...
case BsonType.DateTime:
writer.Write((byte)0x09);
this.WriteCString(writer, key);
var date = (DateTime)value.RawValue;
// do not convert to UTC min/max date values - #19
var utc = (date == DateTime.MinValue || date == DateTime.MaxValue) ? date : date.ToUniversalTime();
var ts = utc - BsonValue.UnixEpoch;
writer.Write(Convert.ToInt64(ts.TotalMilliseconds));
break;
...
}
-
反序列化
BsonDocument Deserialize(byte[] bson, bool utcDate = false)
...
...
else if (type == 0x09) // DateTime
{
var ts = reader.ReadInt64();
// catch specific values for MaxValue / MinValue #19
if (ts == 253402300800000) return DateTime.MaxValue;
if (ts == -62135596800000) return DateTime.MinValue;
var date = BsonValue.UnixEpoch.AddMilliseconds(ts);
return _utcDate ? date : date.ToLocalTime();
}
...
LiteDB 的存取逻辑相对简单,序列化逻辑与 MongoDB 的默认逻辑一致,但读取逻辑默认取到的是本地时间。