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:
{
"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:
{
"myDate": "2018-11-23T20:56:05.311Z"
}
Typically, you would deserialize a Json to Bson with the following code:
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:
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:
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:
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;
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]))
{
if (DateTime.TryParse(_currentStringValue, out var parsedDateTime))
{
_currentDateTime = new BsonDateTime(parsedDateTime);
CurrentBsonType = BsonType.DateTime;
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:
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:
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