I am having issues with hydrating a class which consists of a composite ID which in turn has a base class, I am getting an error saying InvalidOperationException: {document}.Identity is not supported.
The class i am trying to write to the database is 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 has an ID Sku
which is a class formed of the below composite values (VendorId
and a local Value
within 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 entities Identity
which i use in my DDD libraries, essentially the ToString() output here could be used as the ID if this would simplify things:
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 the mappings on app start:
// 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 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 overly complicated by me persisting direct from my entities layer (or if i just use an automapper and simpler POCO)... or if I am missing some mapping directives.
Appreciate any help or pointers.
I was looking at the hydration via constructor post which is done through GetProperties
.
So public readonly Sku Sku;
doesn't show up through classMap.ClassType.GetTypeInfo().GetProperties(_bindingFlags)
because it is only can be accessed as member field.
You can change it to public Sku Sku { get; }
so it is hydrated through constructor via GetProperties
and change all the readonly fields (Sku - VendorId, Value
& VendorId - Value
fields) to have property getter method.
Also, You've to add cm.MapProperty(c => c.Identity)
so x=>x.Identity.Equals(entity.Identity)
can be serialized when used as expression because Identity
cannot be hydrated and registered through ImmutablePocoConvention
as it is not a constructor arg when automap logic runs.
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);
});
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 up the mapping here and support for readonly fields via constructor, for more complex scenarios and manual POCO mapping 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 a singleton) derives from the below base (any queries or writes are done 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 domain 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 VendorID:
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 entity and identity types:
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With