2 / 7
Jul 2024

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?

13 days later

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.

10 days later

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.