MongoDB Composite Key: InvalidOperationException: {document} .Identity is not supported.

I'm having problems wetting the class, which consists of a composite identifier, which in turn has a base class, I get the error InvalidOperationException: {document}.Identity is not supported.

The class I'm trying to write to the database is given below:

 public class Product : IEntity<Product> { public readonly Sku Sku; public string Name { get; private set; } public string Description { get; private set; } public bool IsArchived { get; private set; } public Identity<Product> Identity => Sku; public Product(Sku sku, string name, bool isArchived) { Sku = sku; Name = name; IsArchived = isArchived; } } public interface IEntity<T> { Identity<T> Identity { get; } } 

In turn, it has the identifier Sku , which is a class formed from the following composite values ​​( VendorId and local Value inside Sku ):

 public class Sku : Identity<Product> { public readonly VendorId VendorId; public readonly string Value; public Sku(VendorId vendorId, string value) { VendorId = vendorId; Value = value; } protected override IEnumerable<object> GetIdentityComponents() { return new object[] {VendorId, Value}; } } public class VendorId : Identity<Vendor> { public readonly string Value; public VendorId(string value) { Value = value; } protected override IEnumerable<object> GetIdentityComponents() { return new object[] {Value}; } } 

I have a base class for my Identity entities that I use in my DDD libraries, essentially the output of ToString () here can be used as an identifier if this simplifies the situation:

 public abstract class Identity<T> : IEquatable<Identity<T>> { public override bool Equals(object obj) { /* snip */ } public bool Equals(Identity<T> other) { /* snip */ } public override int GetHashCode() { /* snip */ } public override string ToString() { var id = string.Empty; foreach (var component in GetIdentityComponents()) { if (string.IsNullOrEmpty(id)) id = component.ToString(); // first item, dont add a divider else id += "." + component; } return id; } protected abstract IEnumerable<object> GetIdentityComponents(); } 

I register mappings when the application starts:

 // rehydrate readonly properties via matched constructor // https://stackoverflow.com/questions/39604820/serialize-get-only-properties-on-mongodb ConventionRegistry .Register(nameof(ImmutablePocoConvention), new ConventionPack { new ImmutablePocoConvention() }, _ => true); BsonClassMap.RegisterClassMap<Product>(cm => { cm.AutoMap(); cm.MapIdMember(c => c.Sku); }); BsonClassMap.RegisterClassMap<Vendor>(cm => { cm.AutoMap(); cm.MapIdMember(c => c.Id); }); 

However, when I go and write, I get an InvalidOperationException: {document}.Identity is not supported.

 // my respositoru method public void Upsert<T>(T entity) where T : IEntity<T> { this.Database .GetCollection<T>(product.GetType().FullName)() .ReplaceOneAsync(x=>x.Identity.Equals(entity.Identity), entity, new UpdateOptions() {IsUpsert = true}) .Wait(); } var product = new Product(new Sku(new VendorId("dell"), "12434" ),"RAM", false ); myProductRepo.Upsert(product); 

Not sure if this is now too complicated for me, persisting right from the level of my entities (or if I just use automapper and simpler POCO) ... or if I miss some matching directives.

Appreciate any help or pointers.

+5
source share
2 answers

I looked at hydration through a constructor post that runs through GetProperties .

So public readonly Sku Sku; not displayed through classMap.ClassType.GetTypeInfo().GetProperties(_bindingFlags) , because it can only be accessed as a member field.

You can change it to public Sku Sku { get; } public Sku Sku { get; } so that it is hydrated through the constructor via GetProperties and changes all readonly fields ( Sku - VendorId, Value and VendorId - Value ) to have a getter method.

In addition, you must add cm.MapProperty(c => c.Identity) , so x=>x.Identity.Equals(entity.Identity) can be serialized when used as an expression, because Identity cannot be humidified and registered via ImmutablePocoConvention , since it is not a constructor argument when automatic logic is executed.

Code Changes:

 public class Sku : Identity<Product> { public VendorId VendorId { get; } public string Value { get; } } public class VendorId : Identity<Vendor> { public string Value { get; } } BsonClassMap.RegisterClassMap<Product>(cm => { cm.AutoMap(); cm.MapIdMember(c => c.Sku); cm.MapProperty(c => c.Identity); }); 
+3
source

Here is the code I used:

 public class ProductMongoRepository : IProductRepository { public ICollection<Product> SearchBySkuValue(string sku) { return ProductsMongoDatabase.Instance.GetEntityList<Product>(); } public Product GetBySku(Sku sku) { var collection = ProductsMongoDatabase.Instance.GetCollection<Product>(); return collection.Find(x => x.Sku.Equals(sku)).First(); } public void SaveAll(IEnumerable<Product> products) { foreach (var product in products) { Save(product); } } public void Save(Product product) { var collection = ProductsMongoDatabase.Instance.GetCollection<Product>(); collection .ReplaceOneAsync( x => x.Sku.Equals(product.Sku), product, new UpdateOptions() { IsUpsert = true }) .Wait(); } } 

Setting the display here and supporting readonly fields through the constructor, for more complex scripts and manual POCO matching, we could use BsonSerializer.RegisterSerializer(typeof(DomainEntityClass), new CustomerSerializer());

 public sealed class ProductsMongoDatabase : MongoDatabase { private static volatile ProductsMongoDatabase instance; private static readonly object SyncRoot = new Object(); private ProductsMongoDatabase() { BsonClassMap.RegisterClassMap<Sku>(cm => { cm.MapField(c => c.VendorId); cm.MapField(c => c.SkuValue); cm.MapCreator(c => new Sku(new VendorId(c.VendorId.VendorShortname), c.SkuValue)); }); BsonClassMap.RegisterClassMap<VendorId>(cm => { cm.MapField(c => c.VendorShortname); cm.MapCreator(c => new VendorId(c.VendorShortname)); }); BsonClassMap.RegisterClassMap<Product>(cm => { cm.AutoMap(); cm.MapIdMember(c => c.Sku); cm.MapCreator(c => new Product(c.Sku, c.Name, c.IsArchived)); }); BsonClassMap.RegisterClassMap<Vendor>(cm => { cm.AutoMap(); cm.MapIdMember(c => c.Id); cm.MapCreator(c => new Vendor(c.Id, c.Name)); }); } public static ProductsMongoDatabase Instance { get { if (instance != null) return instance; lock (SyncRoot) { if (instance == null) instance = new ProductsMongoDatabase(); } return instance; } } } 

The above implementation (which is singleton) is derived from the database below (any queries or records are executed in the parent implementation):

 public abstract class MongoDatabase { private readonly IConfigurationRepository _configuration; private readonly IMongoClient Client; private readonly IMongoDatabase Database; protected MongoDatabase() { //_configuration = configuration; var connection = "mongodb://host:27017"; var database = "test"; this.Client = new MongoClient(); this.Database = this.Client.GetDatabase(database); } public List<T> GetEntityList<T>() { return GetCollection<T>() .Find(new BsonDocument()).ToList<T>(); } public IMongoCollection<T> GetCollection<T>() { return this.Database.GetCollection<T>(typeof(T).FullName); } } 

My Sku domain model:

 public class Sku : Identity<Product> { public readonly VendorId VendorId; public readonly string SkuValue; public Sku(VendorId vendorId, string skuValue) { VendorId = vendorId; SkuValue = skuValue; } protected override IEnumerable<object> GetIdentityComponents() { return new object[] {VendorId, SkuValue}; } } 

My product model:

 public class Product : IEntity<Product> { public readonly Sku Sku; public string Name { get; private set; } public bool IsArchived { get; private set; } public Product(Sku sku, string name, bool isArchived) { Sku = sku; Name = name; IsArchived = isArchived; } public void UpdateName(string name) { Name = name; } public void UpdateDescription(string description) { Description = description; } public void Archive() { IsArchived = true; } public void Restore() { IsArchived = false; } // this is used by my framework, not MongoDB public Identity<Product> Identity => Sku; } 

My vendor id:

 public class VendorId : Identity<Vendor> { public readonly string VendorShortname; public VendorId(string vendorShortname) { VendorShortname = vendorShortname; } protected override IEnumerable<object> GetIdentityComponents() { return new object[] {VendorShortname}; } } 

Then I have my entities and types of identifiers:

 public interface IEntity<T> { Identity<T> Identity { get; } } public abstract class Identity<T> : IEquatable<Identity<T>> { private const string IdentityComponentDivider = "."; public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (ReferenceEquals(null, obj)) return false; if (GetType() != obj.GetType()) return false; var other = obj as Identity<T>; return other != null && GetIdentityComponents().SequenceEqual(other.GetIdentityComponents()); } public override string ToString() { var id = string.Empty; foreach (var component in GetIdentityComponents()) { if (string.IsNullOrEmpty(id)) id = component.ToString(); // first item, dont add a divider else id += IdentityComponentDivider + component; } return id; } protected abstract IEnumerable<object> GetIdentityComponents(); public override int GetHashCode() { return HashCodeHelper.CombineHashCodes(GetIdentityComponents()); } public bool Equals(Identity<T> other) { return Equals(other as object); } } 
+1
source

Source: https://habr.com/ru/post/1270701/


All Articles