Protobuf-net не десериализует DateTime.Kind правильно

используя protobuf-net.dll Версия 1.0.0.280

Когда я десериализую DateTime (завернутый в объект), дата/время в порядке, но свойство DateTime.Kind - "Unspecified"

Рассмотрим этот тестовый пример для сериализации/десериализации DateTime.

[TestMethod]
public void TestDateTimeSerialization()
{
    var obj = new DateTimeWrapper {Date = DateTime.UtcNow};
    obj.Date = DateTime.SpecifyKind(obj.Date, DateTimeKind.Utc);
    var serialized = obj.SerializeProto();
    var deserialized = serialized.DeserializeProto<DateTimeWrapper>();
    Assert.AreEqual(DateTimeKind.Utc, deserialized.Date.Kind);
}

public static byte[] SerializeProto<T>(this T item) where T : class
{
    using (var ms = new MemoryStream())
    {
        Serializer.Serialize(ms, item);
        return ms.ToArray();
    }
}

public static T DeserializeProto<T>(this byte[] raw) where T : class, new()
{
    using (var ms = new MemoryStream(raw))
    {
        return Serializer.Deserialize<T>(ms);
    }
}

Ошибка Assert, тип == Unspecified

Добавление

В результате protobuf-net, не сериализующее это свойство (см. ниже), одно из решений состоит в том, чтобы просто предположить, что DateTimeKind равно Utc при отображении дат на стороне клиента (только там, где вы знаете он должен быть UTC, конечно):

public static DateTime ToDisplayTime(this DateTime utcDateTime, TimeZoneInfo timezone)
{
    if (utcDateTime.Kind != DateTimeKind.Utc)//may be Unspecified due to serialization
        utcDateTime = DateTime.SpecifyKind(utcDateTime, DateTimeKind.Utc);
    DateTime result = TimeZoneInfo.ConvertTime(utcDateTime, timezone);
    return result;
}

Это избавит вас от необходимости назначать каждому свойству DateTime на принимающей стороне.

Ответы

Ответ 1

protobuf.net должен поддерживать совместимость с бинарным форматом protobuf, который предназначен для типов дат/времени Java. Поле Kind в Java → Нет Kind поддержка в бинарном формате protobuf → Kind не передается по сети. Или что-то в этом роде.

Как выясняется, protobuf.net кодирует поле Ticks (только), вы найдете код в BclHelpers.cs.

Но не стесняйтесь добавлять еще одно поле в определение сообщения protobuf для этого значения.

Ответ 2

В качестве продолжения ответа Бена... строго говоря, protobuf не имеет определения времени, поэтому нет никакой совместимости с ним. Я испытываю соблазн добавить поддержку для этого в v2, но, к сожалению, он добавил бы 2 байта на каждое значение. Мне еще нужно подумать, приемлемо ли это... например, я мог бы по умолчанию "неуказан", так что только явные локальные или даты UTC имеют значение.

Ответ 3

Здесь реализована реализация для обходного пути. Дайте мне знать, сможете ли вы найти лучшее решение. Спасибо!

[ProtoContract(SkipConstructor = true)]
public class ProtoDateTime
{
    [ProtoIgnore]
    private DateTime? _val;

    [ProtoIgnore]
    private DateTime Value
    {
        get
        {
            if (_val != null)
            {
                return _val.Value;
            }
            lock (this)
            {
                if (_val != null)
                {
                    return _val.Value;
                }
                _val = new DateTime(DateTimeWithoutKind.Ticks, Kind);
            }
            return _val.Value;
        }
        set
        {
            lock (this)
            {
                _val = value;
                Kind = value.Kind;
                DateTimeWithoutKind = value;
            }
        }
    }

    [ProtoMember(1)]
    private DateTimeKind Kind { get; set; }
    [ProtoMember(2)]
    private DateTime DateTimeWithoutKind { get; set; }


    public static DateTime getValue(ref ProtoDateTime wrapper)
    {
        if (wrapper == null)
        {
            wrapper = new ProtoDateTime();
        }
        return wrapper.Value;
    }

    public static DateTime? getValueNullable(ref ProtoDateTime wrapper)
    {
        if (wrapper == null)
        {
            return null;
        }
        return wrapper.Value;

    }

    public static void setValue(out ProtoDateTime wrapper, DateTime value)
    {
        wrapper = new ProtoDateTime { Value = value };
    }

    public static void setValue(out ProtoDateTime wrapper, DateTime? newVal)
    {
        wrapper = newVal.HasValue ? new ProtoDateTime { Value = newVal.Value } : null;
    }
}

Использование:

[ProtoContract(SkipConstructor = true)]
public class MyClass
{
    [ProtoMember(3)]
    [XmlIgnore]
    private ProtoDateTime _timestampWrapper { get; set; }
    [ProtoIgnore]
    public DateTime Timestamp
    {
        get
        {
            return ProtoDateTime.getValue(ref _timestampWrapper);
        }
        set
        {
            return ProtoDateTime.setValue(out _timestampWrapper, value);
        }
    }

    [ProtoMember(4)]
    [XmlIgnore]
    private ProtoDateTime _nullableTimestampWrapper { get; set; }
    [ProtoIgnore]
    public DateTime? NullableTimestamp
    {
        get
        {
            return ProtoDateTime.getValueNullable(ref _nullableTimestampWrapper);
        }
        set
        {
            return ProtoDateTime.setValue(out _nullableTimestampWrapper, value);
        }
    }

}

Ответ 4

Для protobuf может быть больше смысла автоматически десериализовать DateTime с помощью UtcKind, таким образом, если вы используете Utc как свою базу, и я думаю, что это лучшая практика, вы не будете иметь никаких проблем.

Ответ 5

Другое решение - изменить свойство kind для DTO и всегда устанавливать его в UTC. Это может быть неприемлемо для всех приложений, но работает для меня

class DateTimeWrapper 
{
    private DateTime _date;

    public DateTime Date 
    {
        get { return _date; }
        set { _date = new DateTime(value.Ticks, DateTimeKind.Utc);}
    }
}

Update

После использования protobuf более года и интеграции С#, Java, Python и Scala я пришел к выводу, что для DateTime следует использовать длинное представление. Например, используя время UNIX. Мне больно переводить объект С# DateTime protobuf на другие языки DateTime. Тем не менее, что-то столь же простое, как и все, понимается всеми.

Ответ 6

Предполагая, что вам нужен только один DateTimeKind (т.е. UTC или Local), есть простое (хотя и не очень) решение.

Поскольку внутренне protobuf-net преобразует DateTime в представление Unix-Time, оно имеет одно значение DateTime, представляющее эпоху Unix (1970/01/01), к которой каждый раз добавляет соответствующую дельта.

Если вы замените это значение с помощью отражения с помощью значения UTC или Local DateTime, все ваши DateTime будут иметь указанный DateTimeKind:

typeof (BclHelpers).
    GetField("EpochOrigin", BindingFlags.NonPublic | BindingFlags.Static).
    SetValue(null, new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc));

Вы можете узнать больше о моем блоге