[CSHARP-3982] Discriminator not included when saving IReadOnlyList<T> properties Created: 02/Dec/21  Updated: 27/Oct/23  Resolved: 17/Dec/21

Status: Closed
Project: C# Driver
Component/s: Serialization
Affects Version/s: 2.14.0
Fix Version/s: None

Type: Question Priority: Unknown
Reporter: Kevin Smith Assignee: James Kovacs
Resolution: Gone away Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

There seems to be a bug when trying to serialize properties of IReadOnlyList<T> when the property is set to an array of a derived type.

 

For example, running the following code:

using MongoDB.Bson;
using MongoDB.Driver;
 
var client = new MongoClient();
 
var database = client.GetDatabase("test");
 
var collection = database.GetCollection<Farm>("farms");
 
var farm = new Farm(
    ObjectId.GenerateNewId(),
    "Farm 1",
    new[] { new Cow(1, "Bob"), new Cow(2, "Alice") }
);
 
await collection.InsertOneAsync(farm);
 
// Exception thrown here
var foundFarm = await collection.Find(x => x.Id == farm.Id).FirstAsync();
 
record Farm(ObjectId Id, string Name, IReadOnlyList<Animal> Animals);
 
abstract record Animal(int Id);
 
record Cow(int Id, string Name) : Animal(Id);
 
record Pig(int Id, string Name) : Animal(Id);

Throws the following exception:

Unhandled exception. System.FormatException: An error occurred while deserializing the Animals property of class Farm: Cannot create an abstract class.
 ---> System.MemberAccessException: Cannot create an abstract class.
   at System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type type)
   at System.Runtime.Serialization.FormatterServices.GetUninitializedObject(Type type)
   at MongoDB.Bson.Serialization.BsonClassMap.<GetCreator>b__114_2()
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeClass(BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize[TValue](IBsonSerializer`1 serializer, BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.Serializers.EnumerableSerializerBase`2.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize[TValue](IBsonSerializer`1 serializer, BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.Serializers.ImpliedImplementationInterfaceSerializer`2.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.Serializers.SerializerBase`1.MongoDB.Bson.Serialization.IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize(IBsonSerializer serializer, BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeMemberValue(BsonDeserializationContext context, BsonMemberMap memberMap)
   --- End of inner exception stack trace ---
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeMemberValue(BsonDeserializationContext context, BsonMemberMap memberMap)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.DeserializeClass(BsonDeserializationContext context)
   at MongoDB.Bson.Serialization.BsonClassMapSerializer`1.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
   at MongoDB.Bson.Serialization.IBsonSerializerExtensions.Deserialize[TValue](IBsonSerializer`1 serializer, BsonDeserializationContext context)
   at MongoDB.Driver.Core.Operations.CursorBatchDeserializationHelper.DeserializeBatch[TDocument](RawBsonArray batch, IBsonSerializer`1 documentSerializer, MessageEncoderSettings messageEncoderSettings)
   at MongoDB.Driver.Core.Operations.FindCommandOperation`1.CreateFirstCursorBatch(BsonDocument cursorDocument)
   at MongoDB.Driver.Core.Operations.FindCommandOperation`1.CreateCursor(IChannelSourceHandle channelSource, IChannelHandle channel, BsonDocument commandResult)
   at MongoDB.Driver.Core.Operations.FindCommandOperation`1.ExecuteAsync(RetryableReadContext context, CancellationToken cancellationToken)
   at MongoDB.Driver.Core.Operations.FindOperation`1.ExecuteAsync(RetryableReadContext context, CancellationToken cancellationToken)
   at MongoDB.Driver.Core.Operations.FindOperation`1.ExecuteAsync(IReadBinding binding, CancellationToken cancellationToken)
   at MongoDB.Driver.OperationExecutor.ExecuteReadOperationAsync[TResult](IReadBinding binding, IReadOperation`1 operation, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.ExecuteReadOperationAsync[TResult](IClientSessionHandle session, IReadOperation`1 operation, ReadPreference readPreference, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.UsingImplicitSessionAsync[TResult](Func`2 funcAsync, CancellationToken cancellationToken)
   at MongoDB.Driver.IAsyncCursorSourceExtensions.FirstAsync[TDocument](IAsyncCursorSource`1 source, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in C:\dev\kevbite\MongoDbBug\MongoDbBug\MongoDbBug\Program.cs:line 20
   at Program.<Main>(String[] args)

... and generates the following document in the database

{
        "_id" : ObjectId("61a9036e68774829f484e844"),
        "Name" : "Farm 1",
        "Animals" : [
                {
                        "_id" : 1,
                        "Name" : "Bob"
                },
                {
                        "_id" : 2,
                        "Name" : "Alice"
                }
        ]
}

As you can see it's missing the discriminator field (`_t`)

I assume it's because the array is of type `Cow[]` instead of `Animal[]`. If we update the code with the following change:

var farm = new Farm(
    ObjectId.GenerateNewId(),
    "Farm 1",
    new Animal[] { new Cow(1, "Bob"), new Cow(2, "Alice") }
);

The following document is generated

{
        "_id" : ObjectId("61a905ea132842f1fcf18782"),
        "Name" : "Farm 1",
        "Animals" : [
                {
                        "_t" : "Cow",
                        "_id" : 1,
                        "Name" : "Bob"
                },
                {
                        "_t" : "Cow",
                        "_id" : 2,
                        "Name" : "Alice"
                }
        ]
}



 Comments   
Comment by PM Bot [ 17/Dec/21 ]

There hasn't been any recent activity on this ticket, so we're resolving it. Thanks for reaching out! Please feel free to comment on this if you're able to provide more information.

Comment by James Kovacs [ 02/Dec/21 ]

Hi, kev_bite@msn.com,

Thank you for reaching out about your issue. We were able to reproduce the problem that you describe. We appreciate that you shared a self-contained repro of the issue.

The issue is that the driver doesn't know that Animal is the root of a class hierarchy. It also doesn't know that it must create and register ClassMap<T> objects for the derived types. The easiest way to make the driver aware of these two facts is by adorning the base class as follows:

[BsonDiscriminator(RootClass=true)]
[BsonKnownTypes(typeof(Cow), typeof(Pig))]
abstract record Animal(int Id);
 
record Cow(int Id, string Name) : Animal(Id);
 
record Pig(int Id, string Name) : Animal(Id);

If you prefer code-based configuration for ClassMap<T>, you can use code similar to the following when bootstrapping your application:

BsonClassMap.RegisterClassMap<Animal>(cm => {
    cm.AutoMap();
    cm.SetIsRootClass(true);
});
BsonClassMap.RegisterClassMap<Cow>();
BsonClassMap.RegisterClassMap<Pig>();

You can read more about mapping class hierarchies with the driver in Polymorphism in our documentation.

Please let us know if this resolves your issue.

Sincerely,
James

Generated at Wed Feb 07 21:46:50 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.