Search code examples
c#mongodbmongodb-.net-driver

Element 'id' does not match any field or property of error with nested classes


I've the following mongodb document schema;

{ 
    "_id" : ObjectId("5c9d34ff781318afb9e8ab43"), 
    "name" : "Name", 
    "slug" : "slug",
    "services" : {
        "subservice" : {
            "id" : NumberInt(37030)
        }
    }
}

and then i define my classes as;

public class MainModel
{
    public ObjectId Id { get; set; }

    [BsonElement("name")]
    public string Name { get; set; }

    [BsonElement("slug")]
    public string Slug { get; set; }

    [BsonElement("services")]
    public ServicesDef Services { get; set; }

    public class ServicesDef 
    {
        [BsonElement("subservice")]
        public SubServiceDef SubService{ get; set; }

        public class SubServiceDef 
        {
            [BsonElement("id")]
            public int Id { get; set; }
        }
    }
}

But somehow when I query the document;

var result = await Repository.FindAsync(x => x.Slug == slug);

That services.subservice.id isn't properly registered and getting

Element 'id' does not match any field or property of class SubServiceDef.

Stuck here and looking for advice.

I think I'm having the same issue with cannot deserialize with the "Id" attribute but seems there is solution yet.


Solution

  • Long story short: it's all about conventions. MongoDB .NET driver exposes static class ConventionRegistry which allows you to register your own conventions (more here). Additionally there are two "built-in" conventions __defaults__ and __attributes__. Digging deeper (driver github) you can find that it registers one quite interesting convention:

    new NamedIdMemberConvention(new [] { "Id", "id", "_id" })
    

    Which means that id members will be considered as regular BSON _id elements.

    How to fix that ?

    You can get rid of default conventions

    ConventionRegistry.Remove("__defaults__");
    

    However automatically you will drop all the other driver conventions which is pretty risky. Alternatively you can create a fake property which will always be empty:

    public class SubServiceDef
    {
        [BsonElement("id")]
        public int Id { get; set; }
    
        [BsonId]
        public ObjectId FakeId { get; set; }
    }
    

    or you can just use BsonNoId attribute which

    Specifies that the class's IdMember should be null.

    [BsonNoId]
    public class SubServiceDef
    {
        [BsonElement("id")]
        public int Id { get; set; }
    }
    

    So the convention will be setting your id as IdMember in class map but then during postprocessing this attribute will force IdMember to be null and your class will get deserialized succesfully