-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Unknown
-
None
-
Affects Version/s: 3.10.0
-
Component/s: Serialization
-
None
-
None
-
Dotnet Drivers
-
None
-
None
-
None
-
None
-
None
-
None
Summary
BsonClassMapSerializer<TClass>.Serialize ignores BsonSerializationArgs.SerializeAsNominalType.
The flag is documented as "whether to serialize the value as if it were an instance of the nominal type", and it is honored by ClassSerializerBase<TValue>.Serialize and by EnumerableSerializerBase. BsonClassMapSerializer instead always delegates to the actual type's serializer when value.GetType() != typeof(TClass), without consulting the flag:
var actualType = value.GetType(); if (actualType == typeof(TClass)) { SerializeClass(context, args, value); return; } var actualTypeSerializer = this.LookupSerializerInSameDomain(actualType); actualTypeSerializer.Serialize(context, args, value);
As a consequence, SerializeAsNominalTypeSerializer<TActualType, TNominalType> — whose Serialize sets the flag and delegates to the nominal type serializer — cannot work when the nominal type is class-mapped: the class map serializer delegates back to the actual type's serializer, which is the SerializeAsNominalTypeSerializer itself, and the process crashes with StackOverflowException.
Please provide the version of the driver. If applicable, please provide the MongoDB server version and topology (standalone, replica set, or sharded cluster).
Verified on 2.28.0 and 3.x; still present on current master by code inspection. Not server related: the bug is entirely client-side, in BSON serialization (no server involved in the repro).
How to Reproduce
using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; public class Animal { public string Name { get; set; } } public class Cat : Animal { } public static class Program { public static void Main() { BsonSerializer.RegisterSerializer( typeof(Cat), new SerializeAsNominalTypeSerializer<Cat, Animal>()); // crashes the process with StackOverflowException var bson = new Cat { Name = "Tommy" }.ToBson(typeof(Animal)); } }
Call flow:
- BsonClassMapSerializer<Animal>.Serialize(cat): actualType Cat != Animal, looks up the serializer registered for Cat
- SerializeAsNominalTypeSerializer<Cat, Animal>.Serialize: sets args.SerializeAsNominalType = true, delegates to the Animal serializer
- BsonClassMapSerializer<Animal>.Serialize(cat): flag ignored, looks up the serializer for Cat again
- ... infinite recursion, StackOverflowException
Additional Background
This is a longstanding inconsistency rather than a recent regression: ClassSerializerBase has honored the flag since the 2.x serialization API, while BsonClassMapSerializer has never consulted it (verified on v2.28.0, 3.x, and current master).
Proposed fix, one line, consistent with ClassSerializerBase and EnumerableSerializerBase:
if (actualType == typeof(TClass) || args.SerializeAsNominalType) { SerializeClass(context, args, value); return; }
Two notes on the fix's safety:
- the flag does not leak into members: SerializeClass serializes each member through IBsonSerializerExtensions.Serialize(context, value), which creates fresh default args, so polymorphic members keep their standard behavior;
- the flag defaults to false everywhere, so nothing changes for callers that don't set it explicitly.
Besides fixing SerializeAsNominalTypeSerializer, honoring the flag in class maps gives mapping layers built on the driver a supported way to serialize runtime-generated subclasses (e.g. lazy-loading proxy models generated with Castle DynamicProxy or a source generator) through the class map of their concrete base type, without registering a shadow class map for every proxy type. This is the serializer-side half of what CSHARP-3153 asks for.
- related to
-
CSHARP-3153 Add support for serialization of proxy models
-
- Backlog
-