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

с использованием protobuf-net.dll версии 1.0.0.280

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

Рассмотрим этот тестовый пример для сериализации/десериализации 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 на принимающей стороне.


person wal    schedule 12.07.2011    source источник


Ответы (7)


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

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

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

person Ben Voigt    schedule 12.07.2011
comment
Спасибо. Знаете ли вы о другой сериализации с потерями в результате двоичного формата protobuf? - person wal; 12.07.2011
comment
Я недавно столкнулся с этой проблемой. Если DateTimeKind не является Unspecified, должно быть безопасно вызывать ToUniversalTime() при использовании десериализованного DateTime. Это установит DateTimeKind в Utc, и это будет правильно, потому что Ticks не меняется в зависимости от DateTimeKind. ПРИМЕЧАНИЕ. Это не обязательно будет безопасным, если DateTimeKind будет Unspecified до сериализации. - person Brian McBrayer; 16.03.2016

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

person Marc Gravell    schedule 12.07.2011
comment
Марк, почему DateTime обрабатывается иначе, чем пользовательский класс, созданный пользователем (который имеет нулевую «совместимость»)? Я ожидал, что приоритетом № 1 перед размером/скоростью будет воссоздание объекта в его исходном состоянии. - person wal; 12.07.2011
comment
@wal, возможно, в большинстве случаев это просто не было проблемой. Я не против добавления вида к данным v2, если нам это нужно — просто упомянул о влиянии, и все. - person Marc Gravell; 12.07.2011
comment
Обычно даты хранятся в формате UTC. Когда они отправляются по сети, protobuf теряет эту информацию (Kind), что означает, что отображение ее на стороне клиента в их часовом поясе не будет работать. Я удивлен, что никто не сталкивался с этим раньше? - person wal; 12.07.2011
comment
@MarcGravell, что-нибудь изменилось в текущей версии? В настоящее время я сталкиваюсь с точно такой же проблемой, я сериализую utc и возвращаюсь без указания. Есть ли какой-нибудь простой обходной путь? Спасибо - person Matt; 28.04.2014

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

class DateTimeWrapper 
{
    private DateTime _date;

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

Обновлять

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

person oleksii    schedule 02.05.2014

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

[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);
        }
    }

}
person Oron Nadiv    schedule 27.07.2012

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

person Ross Jones    schedule 05.08.2012

Предполагая, что вам нужен только один 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));

Подробнее об этом можно узнать в моем блоге.

person i3arnon    schedule 05.10.2015

Начиная с protobuf-net 2.2 (см. коммит), есть возможность согласие на сериализацию DateTime.Kind. Вы можете установить глобальный флаг. Соответствующая проблема на github (все еще открыта).

А вот пример использования в связи с NServiceBus.

Отказ от ответственности: это не поможет для старой версии protobuf-net, на которую ссылается OP, но это старый вопрос, который может быть полезен для других.

person Hermann.Gruber    schedule 25.02.2019