Question

I'm using latest versions of NodaTime and Mongo DB Official Driver. I have a simple POCO class which uses NodaTime's ZonedDateTime as a replacement for the .NET DateTime in a few Properties.

public class MyPOCO
{
    [BsonId]
    [Key]
    public ObjectId SomeId { get; set; }

    public string SomeProperty { get; set; }

    public ZonedDateTime SomeDateTime { get; set; }
}

I can put the Model into the collection easily, but when I try to read queried Models, I get the following MongoDB.Bson.BsonSerializationException:

Value class NodaTime.ZonedDateTime cannot be deserialized

What`s a good or the best practice to solve/work around this issue?

UPDATE

After posting my solution to the problem, I'm facing a possible new issue... When I query the collection and use the DateTime in my query, like where SomeDateTime < now' (wherenowis a variable I create from system time) it seems that each document must be deserialized using myZonedDateTimeSerializer` before the where clause can be evaluated. This looks like a big performance issue, doesn't it? I really have to think about going back to the BCL DateTime again, even if it hurts.

UPDATE 2

I'm accepting my solution using ZonedDateTimeSerializer, but I'm not feeling comfortable with NodaTime in combination with MongoDB, while both are great individual solutions. But they just don't work well together at the moment without heavy manipulation.

Was it helpful?

Solution

Nevermind, finally after much reading and experimenting, found it finally out. I wrote a custom BsonBaseSerializer implementation to handle ZonedDateTime.

Here is the code of my ZonedDateTimeSerializer:

/// <summary>
/// Serializer for the Noda
/// </summary>
public class ZonedDateTimeSerializer : BsonBaseSerializer
{
    private static ZonedDateTimeSerializer __instance = new ZonedDateTimeSerializer();

    /// <summary>
    /// Initializes a new instance of the ZonedDateTimeSerializer class.
    /// </summary>
    public ZonedDateTimeSerializer()
    {
    }

    /// <summary>
    /// Gets an instance of the ZonedDateTimeSerializer class.
    /// </summary>
    public static ZonedDateTimeSerializer Instance
    {
        get { return __instance; }
    }

    /// <summary>
    /// Deserializes an object from a BsonReader.
    /// </summary>
    /// <param name="bsonReader">The BsonReader.</param>
    /// <param name="nominalType">The nominal type of the object.</param>
    /// <param name="actualType">The actual type of the object.</param>
    /// <param name="options">The serialization options.</param>
    /// <returns>
    /// An object.
    /// </returns>
    public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
    {
        VerifyTypes(nominalType, actualType, typeof(ZonedDateTime));

        var bsonType = bsonReader.GetCurrentBsonType();
        if (bsonType == BsonType.DateTime)
        {
            var millisecondsSinceEpoch = bsonReader.ReadDateTime();
            return new Instant(millisecondsSinceEpoch).InUtc();
        }

        throw new InvalidOperationException(string.Format("Cannot deserialize ZonedDateTime from BsonType {0}.", bsonType));
    }

    /// <summary>
    /// Serializes an object to a BsonWriter.
    /// </summary>
    /// <param name="bsonWriter">The BsonWriter.</param>
    /// <param name="nominalType">The nominal type.</param>
    /// <param name="value">The object.</param>
    /// <param name="options">The serialization options.</param>
    public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
    {
        if (value == null)
            throw new ArgumentNullException("value");

        var ZonedDateTime = (ZonedDateTime)value;
        bsonWriter.WriteDateTime(ZonedDateTime.ToInstant().Ticks);
    }
}

Don't forget to register the Serializer. I wasn't able to find out how to register the Serializer per type, but you can register it per type, which looks like this:

BsonClassMap.RegisterClassMap<MyPOCO>(cm =>
{
    cm.AutoMap();
    cm.GetMemberMap(a => a.SomeDateTime).SetSerializer(ZonedDateTimeSerializer.Instance);
});

Hope this helps.

OTHER TIPS

Here is a modified version of thmshd´s class, that stores the timezone information as well:

public class ZonedDateTimeSerializer : IBsonSerializer<ZonedDateTime>
{
    public static ZonedDateTimeSerializer Instance { get; } = new ZonedDateTimeSerializer();

    object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return Deserialize(context, args);
    }

    public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, ZonedDateTime value)
    {
        if (value == null)
            throw new ArgumentNullException(nameof(value));

        var zonedDateTime = value;

        SerializeAsDocument(context, zonedDateTime);
    }

    private static void SerializeAsDocument(BsonSerializationContext context, ZonedDateTime zonedDateTime)
    {
        context.Writer.WriteStartDocument();
        context.Writer.WriteString("tz", zonedDateTime.Zone.Id);
        context.Writer.WriteInt64("ticks", zonedDateTime.ToInstant().Ticks);
        context.Writer.WriteEndDocument();
    }

    public ZonedDateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        var bsonType = context.Reader.GetCurrentBsonType();

        if (bsonType != BsonType.Document)
        {
            throw new InvalidOperationException($"Cannot deserialize ZonedDateTime from BsonType {bsonType}.");
        }

        context.Reader.ReadStartDocument();
        var timezoneId = context.Reader.ReadString("tz");
        var ticks = context.Reader.ReadInt64("ticks");
        var timezone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timezoneId);

        if (timezone == null)
        {
            throw new Exception($"Unknown timezone id: {timezoneId}");
        }

        context.Reader.ReadEndDocument();

        return new Instant(ticks).InZone(timezone);
    }

    public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
    {
        if (value == null)
        {
            throw new ArgumentNullException(nameof(value));
        }

        var zonedDateTime = (ZonedDateTime)value;

        SerializeAsDocument(context, zonedDateTime);
    }

    public Type ValueType => typeof(ZonedDateTime);
}

It can be globally registered like so:

BsonSerializer.RegisterSerializer(ZonedDateTimeSerializer.Instance);

Edit: Instead of serializing to a subdocument, it might be better to utilize NodaTimes built-in string parsing.

Serialize:

context.Writer.WriteString(ZonedDateTimePattern.CreateWithInvariantCulture("G", DateTimeZoneProviders.Tzdb).Format(zonedDateTime));

Deserialize:

        var zonedDateTimeString = context.Reader.ReadString();
        var parseResult = ZonedDateTimePattern.CreateWithInvariantCulture("G", DateTimeZoneProviders.Tzdb)n.Parse(zonedDateTimeString);

        if (!parseResult.Success)
        {
            throw parseResult.Exception;
        }

        return parseResult.Value;
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top