Reputation: 21
Unfortunately we didn't add the NodaTime.Serialization configuration to the NEventStore wiring when we started this project.
That means we have JSON documents in NEventStore like this.
Note. Simplified view. Not actually an event representation.
dates.json
{
"$type": "NodatimeIssueTest.Product, NodatimeIssueTest",
"FirstDate": {
"$type": "NodaTime.LocalDate, NodaTime",
"ticks": 12304224000000000,
"calendar": "ISO"
},
"SecondDate": {
"$type": "System.Nullable`1[[NodaTime.LocalDate, NodaTime]], mscorlib",
"ticks": 12304224000000000,
"calendar": "ISO"
},
"OtherDates": [
{
"$type": "NodaTime.LocalDate, NodaTime",
"ticks": 12304224000000000,
"calendar": "ISO"
}
],
"FirstDateTime": {
"$type": "NodaTime.LocalDateTime, NodaTime",
"ticks": 12304734100000000,
"calendar": "ISO"
},
"FirstInstant": {
"$type": "NodaTime.Instant, NodaTime",
"ticks": 12304734100000000
}
}
Instead of this
{
"$type": "NodatimeIssueTest.Product, NodatimeIssueTest",
"FirstDate": "2008-12-28",
"SecondDate": "2008-12-28",
"OtherDates": [
"2008-12-28"
],
"FirstDateTime": "2008-12-28T14:10:10",
"FirstInstant": "2008-12-28T14:10:10Z"
}
Because of this we have hard time to upgrade to latest NodaTime package because we can't deserialize the json document anymore.
One solution would be to read all the NEventStore commits and serialize with correct NodaTime parsers. But if this could be avoided I would be happy.
Another option would be to do custom conversion and data binding.
But that requires low level Nodatime logic. Although we don't need to cover all calendars for example.
Upvotes: 1
Views: 533
Reputation: 21
Solution with custom converters and binder for dates.json
example in question
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using NUnit.Framework;
namespace NodatimeIssueTest
{
[TestFixture]
public class TestClass
{
[Test]
public void Deserialize()
{
var serializer = new JsonSerializer();
serializer.NullValueHandling = NullValueHandling.Ignore;
serializer.TypeNameHandling = TypeNameHandling.Objects;
//Custom converters and binder
serializer.Converters.Add(new NodaLocalDateConverter());
serializer.Converters.Add(new NodaLocalDateTimeConverter());
serializer.Converters.Add(new NodaInstantConverter());
serializer.SerializationBinder = new CustomBinder();
using (var sr = new StreamReader($@"{TestContext.CurrentContext.TestDirectory}\dates.json"))
using (var reader = new JsonTextReader(sr))
{
var product = serializer.Deserialize<Product>(reader);
Assert.AreEqual(new LocalDate(2008, 12, 28), product.FirstDate);
Assert.AreEqual(new LocalDate(2008, 12, 28), product.SecondDate);
Assert.AreEqual(new LocalDate(2008, 12, 28), product.OtherDates[0]);
Assert.AreEqual(new LocalDateTime(2008, 12, 28, 14, 10, 10), product.FirstDateTime);
Assert.AreEqual(Instant.FromUtc(2008, 12, 28, 14, 10, 10), product.FirstInstant);
}
}
}
public class Product
{
public LocalDate FirstDate { get; set; }
public LocalDate? SecondDate { get; set; }
public List<LocalDate> OtherDates { get; set; }
public LocalDateTime FirstDateTime { get; set; }
public Instant FirstInstant { get; set; }
}
public class NodaLocalDateConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.StartObject)
{
var custom = (CustomLocalDate) serializer.Deserialize(reader, typeof(CustomLocalDate));
var dateTime = new DateTime(custom.Ticks, DateTimeKind.Utc).AddTicks(new DateTime(1970, 1, 1).Ticks);
var local = LocalDate.FromDateTime(dateTime, CalendarSystem.Iso);
return local;
}
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(LocalDate) || objectType == typeof(LocalDate?);
}
}
public class NodaLocalDateTimeConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.StartObject)
{
var custom = (CustomLocalDateTime) serializer.Deserialize(reader, typeof(CustomLocalDateTime));
var dateTime = new DateTime(custom.Ticks, DateTimeKind.Utc).AddTicks(new DateTime(1970, 1, 1).Ticks);
var local = LocalDateTime.FromDateTime(dateTime, CalendarSystem.Iso);
return local;
}
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(LocalDateTime) || objectType == typeof(LocalDateTime?);
}
}
public class NodaInstantConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.StartObject)
{
var custom = (CustomInstant) serializer.Deserialize(reader, typeof(CustomInstant));
var dateTime = new DateTime(custom.Ticks, DateTimeKind.Utc).AddTicks(new DateTime(1970, 1, 1).Ticks);
var local = Instant.FromDateTimeUtc(dateTime);
return local;
}
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Instant) || objectType == typeof(Instant?);
}
}
public class CustomBinder : DefaultSerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
switch (typeName)
{
case "NodaTime.LocalDate": return typeof(CustomLocalDate);
case "System.Nullable`1[[NodaTime.LocalDate, NodaTime]]": return typeof(CustomLocalDate);
case "NodaTime.LocalDateTime": return typeof(CustomLocalDateTime);
case "System.Nullable`1[[NodaTime.LocalDateTime, NodaTime]]": return typeof(CustomLocalDateTime);
case "NodaTime.Instant": return typeof(CustomInstant);
case "System.Nullable`1[[NodaTime.Instant, NodaTime]]": return typeof(CustomInstant);
default: return base.BindToType(assemblyName, typeName);
}
}
}
public class CustomLocalDate
{
[JsonProperty("ticks")] public long Ticks { get; set; }
[JsonProperty("calendar")] public string Calendar { get; set; }
}
public class CustomLocalDateTime
{
[JsonProperty("ticks")] public long Ticks { get; set; }
[JsonProperty("calendar")] public string Calendar { get; set; }
}
public class CustomInstant
{
[JsonProperty("ticks")] public long Ticks { get; set; }
}
}
Upvotes: 1