From 51f6d9e2399f8e269c62de9fe17b0777c657b60c Mon Sep 17 00:00:00 2001 From: Henk Kin Date: Wed, 27 May 2020 15:46:36 +0200 Subject: [PATCH] Added support for OwnedEntity cloning Updated Microsoft.EntityFrameworkCore to version 3.1.4 Updated this package to version 0.0.4 --- .../CloneByIdWithIncludesIntegrationTests.cs | 21 ++++- .../CloneEnumerableIntegrationTests.cs | 15 +++- ...Microsoft.EntityFrameworkCore.Tests.csproj | 10 +-- .../TestModels/Address.cs | 2 + .../TestModels/Country.cs | 12 +++ .../TestModels/Customer.cs | 1 + .../TestModels/Money.cs | 12 +++ .../TestModels/Order.cs | 2 + .../TestModels/OrderLine.cs | 1 + .../TestModels/TestDbContext.cs | 31 ++++++- .../DbContextExtensions.cs | 90 ++++++++++++++----- ...loner.Microsoft.EntityFrameworkCore.csproj | 4 +- 12 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Country.cs create mode 100644 EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Money.cs diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneByIdWithIncludesIntegrationTests.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneByIdWithIncludesIntegrationTests.cs index f454118..951b1fd 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneByIdWithIncludesIntegrationTests.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneByIdWithIncludesIntegrationTests.cs @@ -20,6 +20,10 @@ public class CloneByIdWithIncludesIntegrationTests : DbContextTestBase public CloneByIdWithIncludesIntegrationTests() : base(nameof(CloneByIdWithIncludesIntegrationTests)) { + var country = new Country + { + Name = "Netherlands" + }; _customer = new Customer { RowVersion = new[] { byte.MinValue }, @@ -27,7 +31,8 @@ public CloneByIdWithIncludesIntegrationTests() : base(nameof(CloneByIdWithInclud Address = new Address { HouseNumber = 25, - Street = "Street" + Street = "Street", + Country = country }, Orders = new List { @@ -41,13 +46,22 @@ public CloneByIdWithIncludesIntegrationTests() : base(nameof(CloneByIdWithInclud OrderDate = _orderDate, OrderStatus = OrderStatus.Order, TenantId = 1, + InstallationAddress = new Address + { + HouseNumber = 25, + Street = "Street", + Country = country + }, + TotalOrderPrice = new Money{ Amount = 1000m, Currency = "EUR"}, OrderLines = new List { new OrderLine { Quantity = 1, + UnitPrice = new Money{ Amount = 500m, Currency = "EUR"}, Article = new Article { + ArticleTranslations = new List { new ArticleTranslation @@ -66,6 +80,7 @@ public CloneByIdWithIncludesIntegrationTests() : base(nameof(CloneByIdWithInclud new OrderLine { Quantity = 2, + UnitPrice = new Money{ Amount = 250m, Currency = "EUR"}, Article = new Article { ArticleTranslations = new List @@ -109,6 +124,10 @@ public async Task Customer_IncludeEntityWithIncludeForOwnsEntityAddress() // Act var clone = await TestDbContext.CloneAsync(x => x .Include(c => c.Address) + //.ThenInclude(c => c.Country) + .Include(c => c.Orders) + .ThenInclude(c => c.InstallationAddress) + //.ThenInclude(c => c.Country) , _customer.Id); // Assert diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneEnumerableIntegrationTests.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneEnumerableIntegrationTests.cs index e70aec0..9258e22 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneEnumerableIntegrationTests.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneEnumerableIntegrationTests.cs @@ -32,7 +32,11 @@ public CloneEnumerableIntegrationTests() : base(nameof(CloneEnumerableIntegratio Address = new Address { HouseNumber = 25, - Street = "Street" + Street = "Street", + Country = new Country + { + Name = "Netherlands" + } }, Orders = new List { @@ -46,6 +50,15 @@ public CloneEnumerableIntegrationTests() : base(nameof(CloneEnumerableIntegratio OrderDate = _orderDate, OrderStatus = OrderStatus.Order, TenantId = 1, + InstallationAddress = new Address + { + HouseNumber = 25, + Street = "Street", + Country = new Country + { + Name = "Netherlands" + } + }, OrderLines = new List { new OrderLine diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/EntityCloner.Microsoft.EntityFrameworkCore.Tests.csproj b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/EntityCloner.Microsoft.EntityFrameworkCore.Tests.csproj index 863d8b0..c7c40ce 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/EntityCloner.Microsoft.EntityFrameworkCore.Tests.csproj +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/EntityCloner.Microsoft.EntityFrameworkCore.Tests.csproj @@ -7,11 +7,11 @@ - - - - - + + + + + all diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Address.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Address.cs index 7f47737..aa68fbb 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Address.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Address.cs @@ -4,5 +4,7 @@ public class Address { public string Street { get; set; } public int HouseNumber { get; set; } + public Country Country { get; set; } + public int CountryId { get; set; } } } \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Country.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Country.cs new file mode 100644 index 0000000..882fbc0 --- /dev/null +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Country.cs @@ -0,0 +1,12 @@ +namespace EntityCloner.Microsoft.EntityFrameworkCore.Tests.TestModels +{ + + + public class Country + { + public int Id { get; set; } + public byte[] RowVersion { get; set; } + public string Name{ get; set; } + + } +} \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Customer.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Customer.cs index 74f043c..4b5a47c 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Customer.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Customer.cs @@ -9,6 +9,7 @@ public class Customer public byte[] RowVersion { get; set; } public DateTime BirthDate { get; set; } public Address Address { get; set; } + public ICollection Orders { get; set; } = new List(); } } \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Money.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Money.cs new file mode 100644 index 0000000..18df152 --- /dev/null +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Money.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EntityCloner.Microsoft.EntityFrameworkCore.Tests.TestModels +{ + public class Money + { + public string Currency { get; set; } + public decimal Amount { get; set; } + } +} diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Order.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Order.cs index ebb9309..4ebbb42 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Order.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Order.cs @@ -12,9 +12,11 @@ public class Order public DateTime OfferDate { get; set; } public DateTime? OrderDate { get; set; } public OrderStatus OrderStatus { get; set; } + public Address InstallationAddress { get; set; } public bool IsDeleted { get; set; } public int TenantId { get; set; } public string Description { get; set; } + public Money TotalOrderPrice { get; set; } public ICollection OrderLines { get; set; } = new List(); } } diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/OrderLine.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/OrderLine.cs index aba7d7f..8e262ff 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/OrderLine.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/OrderLine.cs @@ -9,5 +9,6 @@ public class OrderLine public int OrderId { get; set; } public Order Order { get; set; } public decimal Quantity { get; set; } + public Money UnitPrice { get; set; } } } \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/TestDbContext.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/TestDbContext.cs index 031b0e6..0140208 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/TestDbContext.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/TestDbContext.cs @@ -20,16 +20,43 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasKey(x => x.Id); modelBuilder.Entity().Property(x => x.Id).ValueGeneratedOnAdd(); modelBuilder.Entity().Property(x => x.RowVersion).IsRowVersion(); - modelBuilder.Entity().OwnsOne(x => x.Address); + modelBuilder.Entity().OwnsOne(x => x.Address, addressBuilder=> + { + addressBuilder.Property(x => x.CountryId).IsRequired(); + addressBuilder.HasOne(a => a.Country) + .WithMany() + .HasForeignKey(address => address.CountryId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + }); + modelBuilder.Entity().HasKey(x => x.Id); modelBuilder.Entity().Property(x => x.Id).ValueGeneratedOnAdd(); modelBuilder.Entity().Property(x => x.RowVersion).IsRowVersion(); + modelBuilder.Entity().OwnsOne(x => x.InstallationAddress, addressBuilder => + { + addressBuilder.Property(x => x.CountryId).IsRequired(); + addressBuilder.HasOne(a => a.Country) + .WithMany() + .HasForeignKey(address => address.CountryId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + }); + modelBuilder.Entity().OwnsOne(x=>x.TotalOrderPrice, moneyBuilder => + { + moneyBuilder.Property(e => e.Amount).IsRequired(); + moneyBuilder.Property(e => e.Currency).IsRequired(); + }); modelBuilder.Entity().HasKey(x => x.Id); modelBuilder.Entity().Property(x => x.Id).ValueGeneratedOnAdd(); modelBuilder.Entity().Property(x => x.RowVersion).IsRowVersion(); - + modelBuilder.Entity().OwnsOne(x => x.UnitPrice, moneyBuilder => + { + moneyBuilder.Property(e => e.Amount).IsRequired(); + moneyBuilder.Property(e => e.Currency).IsRequired(); + }); modelBuilder.Entity
().HasKey(x => x.Id); modelBuilder.Entity
().Property(x => x.Id).ValueGeneratedOnAdd(); modelBuilder.Entity
().Property(x => x.RowVersion).IsRowVersion(); diff --git a/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs b/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs index ab855f0..f848ffc 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs @@ -7,7 +7,9 @@ using System.Threading.Tasks; using EntityCloner.Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace EntityCloner.Microsoft.EntityFrameworkCore { @@ -18,7 +20,8 @@ public static async Task> CloneAsync(this DbContext sour { var entities = await queryable.AsNoTracking().ToListAsync(); - var clonedEntities = source.InternalCloneCollection(new Dictionary(), typeof(TEntity), entities); + var entityType = source.FindCurrentEntityType(typeof(TEntity), null, null); + var clonedEntities = source.InternalCloneCollection(new Dictionary(), null, typeof(TEntity), entityType.DefiningNavigationName, entityType.DefiningEntityType, entities); return clonedEntities.Cast().ToList(); } @@ -26,27 +29,31 @@ public static async Task> CloneAsync(this DbContext sour public static async Task CloneAsync(this DbContext source, TEntity entityOrListOfEntities) where TEntity : class { - if((typeof(TEntity).IsGenericType && typeof(TEntity).GetGenericTypeDefinition() == typeof(IEnumerable<>)) || + IEntityType entityType; + if ((typeof(TEntity).IsGenericType && typeof(TEntity).GetGenericTypeDefinition() == typeof(IEnumerable<>)) || (typeof(TEntity).GetInterfaces().Any(i=>i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))) { - var entityType = (typeof(TEntity).HasElementType ? typeof(TEntity).GetElementType() : typeof(TEntity).GenericTypeArguments[0]) ?? typeof(TEntity); + var entityClrType = (typeof(TEntity).HasElementType ? typeof(TEntity).GetElementType() : typeof(TEntity).GenericTypeArguments[0]) ?? typeof(TEntity); - if (source.Model.FindEntityType(entityType) == null) + entityType = source.FindCurrentEntityType(entityClrType, null, null); + if (entityType == null) { throw new ArgumentException($"Argument should be a known entity of the DbContext", nameof(entityOrListOfEntities)); } - var clonedEntities = source.InternalCloneCollection(new Dictionary(), entityType, (IEnumerable)entityOrListOfEntities); + var clonedEntities = source.InternalCloneCollection(new Dictionary(), null, entityClrType, entityType.DefiningNavigationName, entityType.DefiningEntityType, (IEnumerable)entityOrListOfEntities); return (TEntity)clonedEntities; } - if (source.Model.FindEntityType(typeof(TEntity)) == null) + entityType = source.FindCurrentEntityType(typeof(TEntity), null, null); + + if (entityType == null) { throw new ArgumentException($"Argument should be a known entity of the DbContext", nameof(entityOrListOfEntities)); } - - var clonedEntity = (TEntity)source.InternalClone(entityOrListOfEntities, new Dictionary()); + + var clonedEntity = (TEntity)source.InternalClone(null, entityOrListOfEntities, entityType.DefiningNavigationName, entityType.DefiningEntityType, new Dictionary()); return await Task.FromResult(clonedEntity); } @@ -60,7 +67,8 @@ public static Task CloneAsync(this DbContext source, params ob public static async Task CloneAsync(this DbContext source, Func, IClonableQueryable> includeQuery, params object[] primaryKey) where TEntity : class { - var primaryKeyProperties = source.Model.FindEntityType(typeof(TEntity))?.FindPrimaryKey()?.Properties?.Select(p=>p.PropertyInfo).ToList(); + var entityType = source.FindCurrentEntityType(typeof(TEntity), null, null); + var primaryKeyProperties = entityType?.FindPrimaryKey()?.Properties?.Select(p=>p.PropertyInfo).ToList(); if (primaryKeyProperties == null) { throw new NotSupportedException("CloneAsync only can handle types with PrimaryKey configuration'"); @@ -76,7 +84,7 @@ public static async Task CloneAsync(this DbContext source, Fun var entity = await clonableQueryable.Queryable.SingleAsync(); - var clonedEntity = (TEntity)source.InternalClone(entity, new Dictionary()); + var clonedEntity = (TEntity)source.InternalClone(null, entity, entityType.DefiningNavigationName, entityType.DefiningEntityType, new Dictionary()); return clonedEntity; } @@ -158,27 +166,52 @@ private static Expression MakeBinary(ExpressionType type, Expression left, objec return Expression.MakeBinary(type, left, right); } - private static object InternalClone(this DbContext source, object entity, Dictionary references) + private static object InternalClone(this DbContext source, object parentEntity, object entity, string definingNavigationName, IEntityType definingEntityType, Dictionary references) { if (references.ContainsKey(entity)) { return references[entity]; } - var clonedEntity = source.Entry(entity).CurrentValues.ToObject(); + object clonedEntity; + // in case of owned type + if (!string.IsNullOrEmpty(definingNavigationName) && definingEntityType != null) + { + // TODO: clone entity instead of assigning original reference + clonedEntity = entity; + } + else + { + var entityEntry = source.Entry(entity); + clonedEntity = entityEntry.CurrentValues.ToObject(); + } references.Add(entity, clonedEntity); + // source.CloneOwnedEntityProperties(entity, definingNavigationName, definingEntityType, references, clonedEntity); - source.ResetEntityProperties(entity, clonedEntity); + source.ResetEntityProperties(entity, definingNavigationName, definingEntityType, clonedEntity); - source.ResetNavigationProperties(entity, references, clonedEntity); + source.ResetNavigationProperties(entity, definingNavigationName, definingEntityType, references, clonedEntity); return clonedEntity; } - private static void ResetNavigationProperties(this DbContext source, object entity, Dictionary references, object clonedEntity) + //private static void CloneOwnedEntityProperties(this DbContext source, object entity, string definingNavigationName, IEntityType definingEntityType, Dictionary references, object clonedEntity) + //{ + // foreach (var navigation in source.FindCurrentEntityType(entity.GetType(), definingNavigationName, definingEntityType).GetNavigations()) + // { + // var navigationValue = navigation.PropertyInfo.GetValue(entity); + + // if(navigation.ForeignKey.DeclaringEntityType.DefiningEntityType != null && navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName != null) + // { + // navigation.PropertyInfo.SetValue(clonedEntity, navigationValue); + // } + // } + //} + + private static void ResetNavigationProperties(this DbContext source, object entity, string definingNavigationName, IEntityType definingEntityType, Dictionary references, object clonedEntity) { - foreach (var navigation in source.Model.FindEntityType(entity.GetType()).GetNavigations()) + foreach (var navigation in source.FindCurrentEntityType(entity.GetType(), definingNavigationName, definingEntityType).GetNavigations()) { var navigationValue = navigation.PropertyInfo.GetValue(entity); @@ -194,33 +227,33 @@ private static void ResetNavigationProperties(this DbContext source, object enti { if (navigation.IsCollection()) { - var collection = source.InternalCloneCollection(references, navigation.ClrType.GenericTypeArguments[0], (IEnumerable)navigationValue); + var collection = source.InternalCloneCollection(references, entity, navigation.ClrType.GenericTypeArguments[0], navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName, navigation.ForeignKey.DeclaringEntityType.DefiningEntityType, (IEnumerable)navigationValue); navigation.PropertyInfo.SetValue(clonedEntity, collection); } else { - var clonedPropertyValue = source.InternalClone(navigationValue, references); + var clonedPropertyValue = source.InternalClone(entity, navigationValue, navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName, navigation.ForeignKey.DeclaringEntityType.DefiningEntityType, references); navigation.PropertyInfo.SetValue(clonedEntity, clonedPropertyValue); } } } } - private static IList InternalCloneCollection(this DbContext source, Dictionary references, Type collectionItemType, IEnumerable collectionValue) + private static IList InternalCloneCollection(this DbContext source, Dictionary references, object parentEntity, Type collectionItemType, string definingNavigationName, IEntityType definingEntityType, IEnumerable collectionValue) { var list = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(collectionItemType)); foreach (var item in collectionValue) { - var clonedItemValue = source.InternalClone(item, references); + var clonedItemValue = source.InternalClone(parentEntity, item, definingNavigationName, definingEntityType, references); list.Add(clonedItemValue); } return (IList)ConvertToCollectionType(collectionValue.GetType(), collectionItemType, list); } - private static void ResetEntityProperties(this DbContext source, object entity, object clonedEntity) + private static void ResetEntityProperties(this DbContext source, object entity, string definingNavigationName, IEntityType definingEntityType, object clonedEntity) { - foreach (var property in source.Model.FindEntityType(entity.GetType()).GetProperties()) + foreach (var property in source.FindCurrentEntityType(entity.GetType(), definingNavigationName, definingEntityType).GetProperties()) { if (property.IsConcurrencyToken) { @@ -234,6 +267,19 @@ private static void ResetEntityProperties(this DbContext source, object entity, } } + private static IEntityType FindCurrentEntityType(this DbContext source, Type entityClrType, string definingNavigationName, IEntityType definingEntityType) + { + if (!string.IsNullOrEmpty(definingNavigationName) && definingEntityType != null) + { + var entity = source.Model.FindEntityType(entityClrType, definingNavigationName, definingEntityType); + if(entity != null) + { + return entity; + } + } + return source.Model.FindEntityType(entityClrType); + } + private static void ResetProperty(IProperty property, object entity) { if (property.PropertyInfo == null) diff --git a/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj b/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj index a8c6a84..a57ecf7 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj +++ b/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj @@ -12,14 +12,14 @@ Clone DeepClone Entity Entities Include ThenInclude Core EntityFramework EF Cloning entities using EntityFrameworkCore configuration Henk Kin - 0.0.3 + 0.0.4 true true true snupkg - +