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

戏子无情 提交于 2019-12-05 08:24:43

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 ImmutablePocoConventionas 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);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!