BsonSerializer issue

It’s a little bit complicated story, so I put my suggestion at top. I’ve gone through some part of the source on github so… If you’re interested in detail, please go on and read.

  1. Please don’t register a serializer for a type so automatically, like BsonSerializer.LookupSerializer<T>() is invoked.

  2. If #1 is somehow a must, please provide a way that I can replace the serializer with another one.
    Ref: BsonSerializer.cs, BsonSerializerRegistry.cs (_cache.TryAdd(), _cache.GetOrAdd())

  3. Can I use IBsonSerializationProvider to replace the existing one with mine for a type? (I didn’t try it this way)

  4. Please make DateTimeOffsetSerializer stronger. It has timezone and so BsonDateTime and MongoDb date as well. I think it is better than DateTime in a C# class. And you really should consider to have it supports the conversion.

I’m using NET 5 with C# Driver 2.12.2

The issue all came from the datatype mapping from DateTimeOffset to BsonType.DateTime (BsonDateTime). I have a simplified class like:

class Test
{
    [BsonRepresentation(BsonType.DateTime)]
    public DateTimeOffset ts { get; set; }
}

and I would like to have my code like following in order to get a typed-collection connection:

var col = db.GetCollection<Test>(typeof(Test).Name);

I knew it won’t work and will get the exception says:

‘DateTime is not a valid representation for a DateTimeOffsetSerializer.’

Then I made my own serializer to do it and register it at very beginning of my code. Everything works perfect as I expected.

BsonSerializer.RegisterSerializer<DateTimeOffset>(new MyDateTimeOffsetSerializer());

Today, after review to this line, I felt it might be safer to check the existence before register it. so I modified it become:

if(BsonSerializer.LookupSerializer<DateTimeOffset>() == null)
   BsonSerializer.RegisterSerializer<DateTimeOffset>(new MyDateTimeOffsetSerializer());

Boom, code crashes on the line of RegisterSerializer, all the time. And the exception says:

There is already a serializer registered for type DateTimeOffset

Hi, Jason,

Thank you for reaching out to us about the issue with DateTimeOffset and the .NET/C# driver.

This is expected behaviour from BsonSerializer. When an unregistered type is looked up via BsonSerializer.LookupSerializer<T>() we walk the list of registered IBsonSerializationProvider instances looking for one that implements a serializer for the requested type. DateTimeOffsetSerializer is returned from PrimitiveSerializationProvider. So the first time that you lookup a serializer for DateTimeOffset, we don’t find one registered and register the one from PrimitiveSerializationProvider.

If you want to override this behaviour and provide your own serializer for a type, you can register one at startup either via BsonSerializer.RegisterSerializer<T>(IBsonSerializer<T> serializer) or you can include it in a custom IBsonSerializationProvider registered via BsonSerializer.RegisterSerializationProvider(IBsonSerializationProvider provider). Note that IBsonSerializationProvider instances are consulted in reverse order of registration - e.g. last one first - so that users are able to customize serialization behaviour as needed.

Hopefully that explains why #1 is required. In theory we could provide a way to determine if any IBsonSerializationProvider has a serializer for a particular type by adding additional query methods to the interface, but this would be a breaking change. As well there are catch-all IBsonSerializationProvider implementations that allow us to return a generic serializer if a custom one hasn’t been provided. So in practice it is very uncommon to not find any serializer for a particular type. The intent of the design is to override/customize serialization during application startup.

Regarding question #2, we don’t provide a mechanism to swap serializers as this can lead to race conditions where different serializers could be used to serializer/deserialize instances. The intent of the design is to register all serializers or serialization providers during application startup and that the mapping of type to serializer instance remains stable throughout the application lifetime.

Moving onto question #3, yes, you can implement your own IBsonSerializationProvider to override the built-in DateTimeOffsetSerializer. Since you will register the provider last during your application initialization, your custom provider will be used to find a serializer for DateTimeOffset.

Switching gears a bit for question #4, this is challenging due to the representation mismatch between DateTime, DateTimeOffset, and BsonDateTime. I agree with you that .NET should have used DateTimeOffset for its date-time representation from the start, but Microsoft didn’t. DateTime originally stored the date and time without any timezone information. DateTimeKind was retrofitted later to differentiate between local and UTC. But DateTime is used extensively in .NET code for better or worse. DateTimeOffset includes timezone information, which removes a lot of ambiguity and is more nuanced than the DateTimeKind fix. So I understand your desire to use DateTimeOffset in your applications.

Now how does this relate to MongoDB? MongoDB stores date-time instances as BsonDateTime, which is a 64-bit integer representing the number of milliseconds since the Unix epoch (January 1, 1970) in UTC. (See the BSON spec for details.) There is no timezone information as all date-times are converted to UTC for storage. Although you can use BsonDateTime in your applications, it is more natural to use DateTime. If the DateTime.Kind is local, then we convert to UTC based on the current timezone of the client before storing it to the database. If it is UTC already, we do not perform the conversion.

How does this relate to DateTimeOffset? If we were to store DateTimeOffset as BsonDateTime, we would lose the timezone information as we store BsonDateTime as a simple int64 in UTC. The DateTimeOffsetSerializer serializes values as an array (default), document, or string, which allows us to store the timezone information along with the date-time value itself. You could implement your own custom serializer for DateTimeOffset values and make whatever assumptions about the timezone is appropriate for your application and thus only store the DateTime portion of DateTimeOffset, but we cannot make those simplifying assumptions in a generic way that would work for all users of our driver.

Hopefully this provides some clarity on why serialization behaves the way it does and why we cannot automatically serialize DateTimeOffset values into simple BsonDateTime values in the database. Please let us know if you have any additional questions.

Sincerely,
James