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

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


Share Tweet Send
0 Comments
Loading...
You've successfully subscribed to 隔窗听雨
Great! Next, complete checkout for full access to 隔窗听雨
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.
豫ICP备19045256号-1