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