使用 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 的默认逻辑一致,但读取逻辑默认取到的是本地时间。