C# Driver - Why is BsonKnownTypes attribute needed for polymorphism?

Hello,

Can anyone please explain WHY you have to utilize a class map OR [BsonKnownTypes] attribute on our abstract base class to tell mongodb what the possible child classes can be when the discriminator field _t already conveys this information when serialized to the database?

[BsonKnownTypes(typeof(Cat), typeof(Dog))] <-- why is this needed!?
public class Animal 
{
}

public class Cat : Animal 
{
}

As a side note, We are running integration tests against mongodb and the deserialization without the BsonKnownTypes attribute works fine. If anyone could explain this as well it would be awesome!

Hi @Michael_Fyffe,

You need to define the BSON attribute to tell the serialiser about the classes in the hierarchy for deserialisation. Let’s use an example to show case this, see the following polymorphism:

    [BsonKnownTypes(typeof(Cat), typeof(Dog))]
    public class Pet 
    {
        public string Name {get; set;}
    }
    public class Cat : Pet 
    {
        public string Toy {get; set;}
    }
    public class Dog : Pet 
    {
        public string Toy {get; set;}
    }
    public class House
    {
        public ObjectId Id {get; set;}
        public List<Pet> Pets {get; set;}
    }

If you insert an instance of House with two pets, a Cat and a Dog instance as a list. i.e.

var mypets = new List<Pet>();
mypets.Add(new Cat{ Name="Izzy", Toy="scratchy"} );
mypets.Add(new Dog{ Name="Dotty", Toy="plushy"} );
collection.InsertOne(new House { Pets=mypets});

The discriminators are mapped to each of the pet classes. From then on, the mappings exist for the life of the application. If you find the document (deserialise) right after an insert (serialise) it would work because the previous mappings still registered, which is probably what happens in your integration tests. However, if the application try to deserialise the document without prior mappings, the BSON serialiser wouldn’t know the correct mappings. See also Specifying Known Types.

See this gist:Polymorphism BsonKnownTypes for code snippet example. If you run the program it should be able to serialise/deserialise. If you then comment out part A (the insert) and the BsonKnownTypes line, and re-run the program again, you should get an error about Pet deserialisation. Now uncomment the BsonKnownTypes to restore the line back, and you should be able to deserialise.

Regards,
Wan.

3 Likes

Hey @wan

Awesome explanation. Is there a way to turn off the automapping feature on insert so our integration test project will catch missing bsonknowntype attributes?

Hi @Michael_Fyffe,

Could you elaborate more on what are you trying test ? Are you trying to test whether your class have the attributes ?
Also, it could be useful to provide a code example snippet.

Regards,
Wan.

I’m simply wanting my DataAccess Layer Integration Tests to build more confidence for my team by uncovering missing attributes needed for polymorphism by having the following type of test blow up

    [Test]
    public void GetByXXUniqueIdentifier_XXDocumentExist_ReturnsDocument()
    {
        // Arrange
        var sut = GetRepository<IXXDocumentRepository>();
        var document = GetXXDocuments(1).FirstOrDefault();
        sut.AddOne(document); // But this prevents the below line from blowing up

        // Act
        var actual = sut.GetByXXUniqueIdentifier(document.XXUniqueIdentifier); // I want this to blow up

        // Assert
        actual.ShouldBeEquivalentTo(document);
    }

Hi @Michael_Fyffe,

Depending on your use case, this could be accomplished via unit test instead of integration test. Essentially you would like to test whether the class(es) have the necessary attributes needed for polymorphism. You can use check whether a class have the necessary attribute(s) using reflection. See also Accessing Attributes by Using Reflection.

Using the same example classes above (Pet, Cat, and Dog):

// Checking class Pet
System.Attribute[] attrs = System.Attribute.GetCustomAttributes(typeof(Pet));  

foreach (System.Attribute attr in attrs)  
{  
    if (attr is BsonKnownTypesAttribute)  
    {  
        BsonKnownTypesAttribute a = (BsonKnownTypesAttribute)attr;  
                
        foreach(var item in a.KnownTypes) 
        {
            // Should return Cat and Dog (including the namespace)
            Console.WriteLine(item.ToString());  
        }
     } 
}  

Regards,
Wan.