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));
Вы можете узнать больше о моем блоге