Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / MongoDB

MongoDB C#: How to Deserialize a JSON Containing a DateTime into a Proper BsonDocument DateTime Value

5.00/5 (2 votes)
23 Nov 2018CPOL3 min read 31.4K  
What if your Json contains a datetime value like 2018-11-23T20:56:05.3117673Z and you need it to be in a BsonDocument as a proper BsonDateTime value?

Introduction

This is my very first post and finally I found a problem, that I think many people have in this domain (Json, DateTime and MongoDb with BsonDocument) that is worth posting! I am happy to share a solution with you!

Most of the time, people use Json as a serialization format to transfer data between applications. MongoDB has built-in serializers to convert from Json to Bson. The problem is that the resulting Bson will not handle the intended DateTime string to be a BsonType.DateTime, but instead it will be handled as a BsonType.String. I want to give you an option on how to build something, that properly deserializes Json string values (that are of a given DateTime format) to BsonDateTime values.

Background

The built-in serializers from MongoDb can handle datetime string representations in "Json" when they meet a given format, that is:

JavaScript
{
  "myDate": ISODate("2018-11-23T20:56:05.311Z")
}

But this is not a valid Json format, and no Json serializer (like Json.NET) will be able to serialize or deserialize it properly (without any modification). Most people would like to have the following Json format:

JavaScript
{
  "myDate": "2018-11-23T20:56:05.311Z"
}

Typically, you would deserialize a Json to Bson with the following code:

C#
var bson = BsonSerializer.Deserialize<BsonDocument>(json);

You will see that the result is of BsonType.String.

When you check the source code of BsonSerializer, you will find the following code:

C#
public static TNominalType Deserialize<TNominalType>
    (string json, Action<BsonDeserializationContext.Builder> configurator = null)
{
   using (var bsonReader = new JsonReader(json))
   {
      return Deserialize<TNominalType>(bsonReader, configurator);
   }
}

So under the hood, MongoDb is using some kind of JsonReader, that just parses any string value to BsonType.String.

This is where we will extend the JsonReader to be able to properly parse the above mentioned datetime pattern to result in a BsonType.DateTime.

Using the Code

Let's create our own DateTimeAwareJsonReader, that just extends the existing JsonReader and tries to figure whether a given string value could be a valid date time. But we want to keep in mind that we want it to be as performant as possible. We know that we have to investigate each string representation of a Json value whether it is a datetime or not. So let's get started:

C#
public class DateTimeAwareJsonReader : JsonReader
{
    public DateTimeAwareJsonReader(string json) : base(json)
    {
    }

    public DateTimeAwareJsonReader(TextReader textReader) : base(textReader)
    {
    }

    public DateTimeAwareJsonReader(string json, JsonReaderSettings settings) : base(json, settings)
    {
    }

    public DateTimeAwareJsonReader(TextReader textReader, JsonReaderSettings settings) : 
                                   base(textReader, settings)
    {
    }
}

The serialization engine uses IBsonReader.ReadBsonType() to figure which type is currently being parsed, so we have to hook into it and do our investigation here, whether we would like to tell the engine that the given value is a string or a datetime. So let's add an override:

C#
private string _currentStringValue;
private BsonDateTime _currentDateTime;

public override BsonType ReadBsonType()
{
    _currentDateTime = null;
    var currentBsonType = base.ReadBsonType();
    if (currentBsonType == BsonType.String)
    {
        var previousState = State;
        _currentStringValue = ReadString();
        State = previousState;
        // date pattern is YYYY-MM-dd, if this pattern is met, 
        // we assume it is a DateTime and try to parse.
        // kind of like duck typing..if it walks like a duck and talks like a duck, 
        // it must be a duck.. :D
        if (_currentStringValue.Length > 9)
        {
            if (char.IsDigit(_currentStringValue[0]) &&
                char.IsDigit(_currentStringValue[1]) &&
                char.IsDigit(_currentStringValue[2]) &&
                char.IsDigit(_currentStringValue[3]) &&
                _currentStringValue[4].Equals('-') &&
                char.IsDigit(_currentStringValue[5]) &&
                char.IsDigit(_currentStringValue[6]) &&
                _currentStringValue[7].Equals('-') &&
                char.IsDigit(_currentStringValue[8]) &&
                char.IsDigit(_currentStringValue[9]))
            {
                // looks like a date string
                if (DateTime.TryParse(_currentStringValue, out var parsedDateTime))
                {
                    _currentDateTime = new BsonDateTime(parsedDateTime);
                    CurrentBsonType = BsonType.DateTime; // we have to set also the 
                                                         // current bson type and return it.
                    return BsonType.DateTime;
                }
            }
        }
    }
    return currentBsonType;
}

The base JsonReader will tell us, that our "myDate" property is of BsonType.String. In this case, we want to intercept and investigate a bit further. To make it slightly more efficient, we will only inspect values that are of a given size at least. I expect a serialized datetime to be at least in the format of "YYYY-MM-dd" (e.g., "2018-05-02"). If you have other requirements, you can adjust the logic here as required. So once we have digits involved here, and everything looks like being a string representation of a datetime, we will tell the serializer that this value is a BsonType.DateTime, otherwise we want to fallback to what it would be if the serializer has to decide.

Once we figured the given value being a DateTime, we have to override another method:

C#
public override long ReadDateTime()
{
    if (_currentDateTime == null)
    {
        return base.ReadDateTime();
    }
            
    if (Disposed) { ThrowObjectDisposedException(); }
    VerifyBsonType("ReadDateTime", BsonType.DateTime);
    State = BsonReaderState.Type;
    return _currentDateTime.AsBsonDateTime.MillisecondsSinceEpoch;
}

In case our logic kicks in, the field _currentDateTime will have the parsed value and we will return it.

And this is it! You now have a working solution that will correctly figure a datetime string representation and tell MongoDb to handle it as a DateTime.

To test it, you can do a simple console app like that:

C#
class Program
{
    public class SomeModel
    {
        [JsonProperty("id")]
        public int Id { get; set; }
        [JsonProperty("test")]
        public string Test { get; set; }
        [JsonProperty("today")]
        public DateTime Today { get; set; }
        [JsonProperty("inner")]
        public SomeModel Inner { get; set; }
    }

    static void Main(string[] args)
    {
        var model = new SomeModel
        {
            Id = 1,
            Test = "Model",
            Today = DateTime.UtcNow.Date,
            Inner = new SomeModel
            {
                Id = 2,
                Test = "Inner",
                Today = DateTime.UtcNow
            }
        };

        var json = JsonConvert.SerializeObject(model);
        using (var reader = new DateTimeAwareJsonReader(json))
        {
            var bson = BsonSerializer.Deserialize<BsonDocument>(reader);
            Console.WriteLine("Models today property is: {0}", bson["today"].BsonType);
        }
        Console.ReadLine();
    }
}

Points of Interest

Once I figured how MongoDb serialization works, it was quite easy to build an intercepting mechanism to tell the underlying serialization framework what a datetime is supposed to be. I haven't tested it yet in a productive environment, but I will do soon. The more interesting part is to figure a lean way to guess whether a string value could be a datetime, in the most efficient way without interfering too much with the MongoDb client library. I think this solution will work well for most of the use cases out there. Looking forward to your opinions! ;-)

History

  • 2018-11-23: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)