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 ClassMap
s 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.