Configuring classMap for included subcollection

I have this object model:

PhoneBookContact contact2 = new()
{
    //Id = ObjectId.GenerateNewId().ToString(),
    FirstName = "contact 2",
    Location = "Bern",
    PhoneBookIds= [], //Ids of another collection
    Numbers = [
        new PhoneBookContactNumber { /*Id = ObjectId.GenerateNewId().ToString(),*/ Number = "+41580000003", Type = NumberType.Office },
        new PhoneBookContactNumber { /*Id = ObjectId.GenerateNewId().ToString(),*/ Number = "+41760000004", Type = NumberType.Mobile }
        ]
};

All properties called Id are strings, but should be stored as ObjectId in the database. Do do this for all Id properties in all collections, I’m using a Convention

public void PostProcess(BsonClassMap classMap)
{
    var idMemberMap = classMap.IdMemberMap;
    if (idMemberMap == null || idMemberMap.IdGenerator != null)
        return;
    if (idMemberMap.MemberType == typeof(string))
    {
        idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance).SetSerializer(new StringSerializer(BsonType.ObjectId));
    }
}

To treat the PhoneBookIds, I’m doing this:

            classMap.MapProperty(x => x.PhoneBookIds)
                .SetSerializer(
                    new EnumerableInterfaceImplementerSerializer<List<string>, string>(
                    new StringSerializer(BsonType.ObjectId)));

Now I’d like to have the same treatment for PhoneBookContactNumber.Id. So I tried this:

classMap.MapProperty(x => x.Numbers[0].Number).SetSerializer(new StringSerializer(BsonType.ObjectId));

which doesn’t work. Any idea what I have to do to autogenerate the Id for that included subdocument?

Did you create a convention to auto genrate the ID?

Yes I did.

This is my entire classmap setup for the PhoneBookContact object:

BsonClassMap.RegisterClassMap<PhoneBookContact>(classMap =>
{
    classMap.AutoMap();
    classMap.UnmapMember(m => m.Categories);
    classMap.UnmapMember(m => m.PhoneBooks);
    classMap.MapProperty(m => m.NumberOfTelephoneNumbers);
    classMap.MapProperty(x => x.ManagerId).SetSerializer(new StringSerializer(BsonType.ObjectId));
    //classMap.MapProperty(x => x.Numbers[0].Number).SetSerializer(new StringSerializer(BsonType.ObjectId));

    classMap.MapProperty(x => x.PhoneBookIds)
        .SetSerializer(
            new EnumerableInterfaceImplementerSerializer<List<string>, string>(
            new StringSerializer(BsonType.ObjectId)));
    classMap.MapProperty(x => x.CategoryIds)
        .SetSerializer(
            new EnumerableInterfaceImplementerSerializer<List<string>, string>(
            new StringSerializer(BsonType.ObjectId)));
});

And I register my custom convention as follows:

var pack = new ConventionPack
{
    new IgnoreExtraElementsConvention(true),
    new StringObjectIdGeneratorConvention()
};
ConventionRegistry.Register("My Solution Conventions", pack, t => true);

With StringObjectIdGeneratorConvention being:

internal class StringObjectIdGeneratorConvention : ConventionBase, IPostProcessingConvention
{
    /// <summary>
    /// Applies a post processing modification to the class map.
    /// </summary>
    /// <param name="classMap">The class map.</param>
    public void PostProcess(BsonClassMap classMap)
    {
        var idMemberMap = classMap.IdMemberMap;
        if (idMemberMap == null || idMemberMap.IdGenerator != null)
            return;
        if (idMemberMap.MemberType == typeof(string))
        {
            idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance).SetSerializer(new StringSerializer(BsonType.ObjectId));
        }
    }
}

I’m using conventions and ClassMaps because I cannot add a MongoDb dependency to the POCO object itself.

automatically setting the Id property for subdocuments is missing. Try

public class PhoneBookContact
{
    public string Id { get; set; }
    public string FirstName { get; set; }
    public string Location { get; set; }
    public List<string> PhoneBookIds { get; set; }
    public List<PhoneBookContactNumber> Numbers { get; set; }
    public string ManagerId { get; set; }
    public List<string> CategoryIds { get; set; }
}

public class PhoneBookContactNumber
{
    public string Id { get; set; }
    public string Number { get; set; }
    public NumberType Type { get; set; }
}

public enum NumberType
{
    Office, Mobile, Home
}

public static void RegisterConventions()
{
    var pack = new ConventionPack
    {
        new IgnoreExtraElementsConvention(true),
        new StringObjectIdGeneratorConvention()
    };

    ConventionRegistry.Register("My Solution Conventions", pack, t => true);
}

internal class StringObjectIdGeneratorConvention : ConventionBase, IPostProcessingConvention
{
    public void PostProcess(BsonClassMap cm)
    {
        var idMemberMap = cm.IdMemberMap;
        if (idMemberMap == null || idMemberMap.IdGenerator != null)
            return;
        
        if (idMemberMap.MemberType == typeof(string))
        {
            idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance)
                       .SetSerializer(new StringSerializer(BsonType.ObjectId));
        }
    }
}

public static void ConfigureClassMaps()
{
    BsonClassMap.RegisterClassMap<PhoneBookContactNumber>(cm =>
    {
        cm.AutoMap();
        cm.MapIdMember(c => c.Id)
          .SetIdGenerator(StringObjectIdGenerator.Instance)
          .SetSerializer(new StringSerializer(BsonType.ObjectId));
        cm.MapMember(c => c.Number).SetSerializer(new StringSerializer(BsonType.String));
    });

    BsonClassMap.RegisterClassMap<PhoneBookContact>(cm =>
    {
        cm.AutoMap();
        cm.UnmapMember(m => m.Categories);
        cm.UnmapMember(m => m.PhoneBooks);
        cm.MapProperty(m => m.NumberOfTelephoneNumbers);
        cm.MapProperty(x => x.ManagerId).SetSerializer(new StringSerializer(BsonType.ObjectId));
        
        cm.MapProperty(x => x.PhoneBookIds)
            .SetSerializer(new EnumerableInterfaceImplementerSerializer<List<string>, string>(
                new StringSerializer(BsonType.ObjectId)));
        cm.MapProperty(x => x.CategoryIds)
            .SetSerializer(new EnumerableInterfaceImplementerSerializer<List<string>, string>(
                new StringSerializer(BsonType.ObjectId)));
        
        cm.MapMember(x => x.Numbers).SetSerializer(
            new EnumerableInterfaceImplementerSerializer<List<PhoneBookContactNumber>, PhoneBookContactNumber>(
                BsonSerializer.LookupSerializer<PhoneBookContactNumber>()));
    });
}

I tried your approach. Unfortunately, the Id of the PhoneBookContactNumber is still empty when I add a new PhoneBookContact with a PhoneBookContactNumber

Some questions:

if I take your mapping but swap the order of PhoneBookContactNumber and PhoneBookContact, it errors out (something about a key already having been added to a dictionary). The error being:

System.ArgumentException: ‘An item with the same key has already been added. Key: NoSqlModels.PhoneBookContactNumber’

Any idea what this is about?

Second, any particular reason you’re defining a mapping for the Number property in PhoneBookContactNumber? That’s just a regular old string.

I opened a support case on this. In the end, there’s no way to automatically inject an auto-generated Id to a subdocuments - the automatic Id generation only works on documents. I ended up injecting an Id manually using

number.Id ??= ObjectId.GenerateNewId().ToString()

Ok but this is automatic. You basically created a method to set IDs for subdocuments before inserting the main document.