关于使用|关于使用 C# 读写 MongoDB 时涉及 DateTime 的问题

使用 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 = https://www.it610.com/article/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 = https://www.it610.com/article/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); ... }

【关于使用|关于使用 C# 读写 MongoDB 时涉及 DateTime 的问题】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 的默认逻辑一致,但读取逻辑默认取到的是本地时间。
转载于:https://my.oschina.net/lidanger/blog/3039686

    推荐阅读