diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneRelationshipsTests.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneRelationshipsTests.cs index bfef375..edd5d90 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneRelationshipsTests.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/CloneRelationshipsTests.cs @@ -11,10 +11,6 @@ namespace EntityCloner.Microsoft.EntityFrameworkCore.Tests { public class CloneRelationshipsTests : DbContextTestBase { - private readonly string _localeEnGb = "en-GB"; - private readonly string _localeNlNL = "nl-NL"; - - private readonly Customer _customer; private readonly Blog _blog; private readonly BlogAssets _blogAssets; private readonly Post _post1; @@ -39,8 +35,8 @@ public CloneRelationshipsTests() : base(nameof(CloneRelationshipsTests)) _blogAssets = new BlogAssets { //Id = 2, - Banner = Encoding.UTF8.GetBytes("Dit is een test"), - BlogId = _blog.Id, + Banner = Encoding.UTF8.GetBytes("Dit is een test"), + BlogId = _blog.Id, Blog = _blog }; _blog.Assets = _blogAssets; @@ -79,8 +75,8 @@ public CloneRelationshipsTests() : base(nameof(CloneRelationshipsTests)) TagHeaderId = _tagHeader1.Id, }; - - _tagIpAddress1 = new TagIpAddress { IpAddress = "IpAddress1", Tag = _tag1, TagId = _tag1.Id}; + + _tagIpAddress1 = new TagIpAddress { IpAddress = "IpAddress1", Tag = _tag1, TagId = _tag1.Id }; _tagIpAddress2 = new TagIpAddress { IpAddress = "IpAddress2", Tag = _tag1, TagId = _tag1.Id }; _tagIpAddress3 = new TagIpAddress { IpAddress = "IpAddress3", Tag = _tag2, TagId = _tag2.Id }; @@ -114,7 +110,7 @@ public CloneRelationshipsTests() : base(nameof(CloneRelationshipsTests)) _tag1.Posts.Add(_post2); _tag2.Posts.Add(_post1); _tag2.Posts.Add(_post2); - + _blog.Posts.Add(_post1); _blog.Posts.Add(_post2); @@ -124,11 +120,11 @@ public CloneRelationshipsTests() : base(nameof(CloneRelationshipsTests)) } [Fact] - public async Task Customer_CloneWithoutIncludes() + public async Task Blog_CloneWithoutIncludes() { // Arrange var entity = await TestDbContext.Set() - .Where(c=>c.Id == _blog.Id) + .Where(c => c.Id == _blog.Id) .AsNoTracking() .SingleAsync(); @@ -145,11 +141,11 @@ public async Task Customer_CloneWithoutIncludes() [Fact] - public async Task Customer_CloneWithIncludeOfOptionalOneToManyReferenceRelation() + public async Task Blog_CloneWithIncludeOfOptionalOneToManyReferenceRelation() { // Arrange var entity = await TestDbContext.Set() - .Include(b=>b.Posts) + .Include(b => b.Posts) .Where(c => c.Id == _blog.Id) .AsNoTracking() .SingleAsync(); @@ -182,7 +178,7 @@ public async Task Customer_CloneWithIncludeOfOptionalOneToManyReferenceRelation( [Fact] - public async Task Customer_CloneWithIncludeOfManyToManyReferenceRelation() + public async Task Post_CloneWithIncludeOfManyToManyReferenceRelation() { // Arrange var entity = await TestDbContext.Set() @@ -219,7 +215,7 @@ public async Task Customer_CloneWithIncludeOfManyToManyReferenceRelation() } [Fact] - public async Task Customer_CloneWithIncludeOfManyToManyReferenceRelationWithChildForeignKeyReleation() + public async Task Post_CloneWithIncludeOfManyToManyReferenceRelationWithChildForeignKeyReleation() { // Arrange var entity = await TestDbContext.Set() @@ -266,7 +262,7 @@ public async Task Customer_CloneWithIncludeOfManyToManyReferenceRelationWithChil } [Fact] - public async Task Customer_CloneWithIncludeOfManyToManyReferenceRelationWithChildOneToManyReleation() + public async Task Post_CloneWithIncludeOfManyToManyReferenceRelationWithChildOneToManyReleation() { // Arrange var entity = await TestDbContext.Set() @@ -326,193 +322,5 @@ public async Task Customer_CloneWithIncludeOfManyToManyReferenceRelationWithChil Assert.Equal(0, cloneTag3.TagIpAddresses.Count); } - //[Fact] - //public async Task Customer_IncludeEntityWithIncludeForOwnsEntityAddress() - //{ - // // Arrange - // var entity = await TestDbContext.Set() - // .Include(c => c.Address) - // .Where(c => c.Id == _customer.Id) - // .AsNoTracking() - // .SingleAsync(); - - // // Act - // var clone = await TestDbContext.CloneAsync(entity); - - // // Assert - // Assert.NotNull(clone); - // Assert.NotNull(clone.Address); - //} - - //[Fact] - //public async Task Customer_IncludeEntityWithoutOrderLines() - //{ - // // Act - // var entity = await TestDbContext.Set() - // .Include(c => c.Orders) - // .Where(c => c.Id == _customer.Id) - // .AsNoTracking() - // .SingleAsync(); - - // // Act - // var clone = await TestDbContext.CloneAsync(entity); - - // // Assert - // Assert.NotNull(clone); - // Assert.Empty(clone.Orders.SelectMany(o => o.OrderLines)); - //} - - //[Fact] - //public async Task Customer_IncludeEntityWithOrderLines() - //{ - // // Act - // var entity= await TestDbContext.Set() - // .Include(c => c.Orders) - // .ThenInclude(c => c.OrderLines) - // .Where(c => c.Id == _customer.Id).AsNoTracking() - // .SingleAsync(); - - // // Act - // var clone = await TestDbContext.CloneAsync(entity); - - // // Assert - // Assert.NotNull(clone); - // Assert.Equal(2, clone.Orders.SelectMany(o => o.OrderLines).Count()); - //} - - //[Fact] - //public async Task Customer_IncludeEntityWithoutArticleTranslations() - //{ - // // Act - // var entity = await TestDbContext.Set() - // .Include(c => c.Orders) - // .ThenInclude(c => c.OrderLines) - // .ThenInclude(c => c.Article) - // .Where(c => c.Id == _customer.Id) - // .AsNoTracking() - // .SingleAsync(); - - // // Act - // var clone = await TestDbContext.CloneAsync(entity); - - // // Assert - // Assert.NotNull(clone); - // Assert.Empty(clone.Orders.SelectMany(o => o.OrderLines.SelectMany(ol => ol.Article.ArticleTranslations))); - //} - - //[Fact] - //public async Task Customer_IncludeEntityWithArticleTranslations() - //{ - // // Act - // var entity = await TestDbContext.Set() - // .Include(c => c.Orders) - // .ThenInclude(c => c.OrderLines) - // .ThenInclude(c => c.Article) - // .ThenInclude(c => c.ArticleTranslations) - // .Where(c => c.Id == _customer.Id) - // .AsNoTracking() - // .SingleAsync(); - - // // Act - // var clone = await TestDbContext.CloneAsync(entity); - - // // Assert - // Assert.NotNull(clone); - // Assert.Equal(4, clone.Orders.SelectMany(o => o.OrderLines.SelectMany(ol => ol.Article.ArticleTranslations)).Count()); - //} - - //[Fact] - //public async Task Customer_IncludeEntityWithAllPossibleIncludes() - //{ - // // Act - // var entity = await TestDbContext.Set() - // .Include(c => c.Orders) - // .ThenInclude(c => c.OrderLines) - // .ThenInclude(c => c.Article) - // .ThenInclude(c => c.ArticleTranslations) - // .Include(c => c.Orders) - // .Include(c => c.Address) - // .Where(c => c.Id == _customer.Id) - // .AsNoTracking() - // .SingleAsync(); - - // // Act - // var clone = await TestDbContext.CloneAsync(entity); - - // // Assert - // Assert.NotNull(clone); - - // // Customer - // Assert.Equal(0, clone.Id); - // Assert.Equal(_birthDate, clone.BirthDate); - // Assert.NotNull(clone.Address); - // Assert.Equal(25, clone.Address.HouseNumber); - // Assert.Equal("Street", clone.Address.Street); - // Assert.Equal(1, clone.Orders.Count); - - // // Order - // var order = clone.Orders.Single(); - // Assert.Equal(0, clone.Id); - // Assert.NotNull(order.Customer); - // Assert.Equal(0, order.CustomerId); - // Assert.Equal("Description", order.Description); - // Assert.Equal(OrderStatus.Order, order.OrderStatus); - // Assert.False(order.IsDeleted); - // Assert.Equal(_offerDate, order.OfferDate); - // Assert.Equal(_orderDate, order.OrderDate); - // Assert.Equal(2, order.OrderLines.Count); - - // // OrderLine 1 - // var orderLine1 = order.OrderLines.First(); - - // Assert.Equal(0, orderLine1.Id); - // Assert.NotNull(orderLine1.Order); - // Assert.Equal(0, orderLine1.OrderId); - // Assert.NotNull(orderLine1.Article); - // Assert.Equal(0, orderLine1.ArticleId); - // Assert.Equal(1, orderLine1.Quantity); - - // // Article - // Assert.Equal(0, orderLine1.Article.Id); - // Assert.Equal(2, orderLine1.Article.ArticleTranslations.Count); - - // // ArticleTranslations1 - // var orderLine1Article1ArticleTranslations1 = orderLine1.Article.ArticleTranslations.First(); - // Assert.Null(orderLine1Article1ArticleTranslations1.LocaleId); // is part of PrimaryKey - // Assert.Equal("Artikel 1 en-GB", orderLine1Article1ArticleTranslations1.Description); - // Assert.Equal(0, orderLine1Article1ArticleTranslations1.ArticleId); - - // // ArticleTranslations2 - // var orderLine1Article1ArticleTranslations2 = orderLine1.Article.ArticleTranslations.Last(); - // Assert.Null(orderLine1Article1ArticleTranslations2.LocaleId);// is part of PrimaryKey - // Assert.Equal("Artikel 1 nl-NL", orderLine1Article1ArticleTranslations2.Description); - // Assert.Equal(0, orderLine1Article1ArticleTranslations2.ArticleId); - - // // OrderLine 2 - // var orderLine2 = order.OrderLines.Last(); - - // Assert.Equal(0, orderLine2.Id); - // Assert.NotNull(orderLine2.Order); - // Assert.Equal(0, orderLine2.OrderId); - // Assert.NotNull(orderLine2.Article); - // Assert.Equal(0, orderLine2.ArticleId); - // Assert.Equal(2, orderLine2.Quantity); - - // // Article - // Assert.Equal(0, orderLine2.Article.Id); - // Assert.Equal(2, orderLine2.Article.ArticleTranslations.Count); - - // // ArticleTranslations1 - // var orderLine2Article1ArticleTranslations1 = orderLine2.Article.ArticleTranslations.First(); - // Assert.Null(orderLine2Article1ArticleTranslations1.LocaleId);// is part of PrimaryKey - // Assert.Equal("Artikel 2 en-GB", orderLine2Article1ArticleTranslations1.Description); - // Assert.Equal(0, orderLine2Article1ArticleTranslations1.ArticleId); - - // // ArticleTranslations2 - // var orderLine2Article1ArticleTranslations2 = orderLine2.Article.ArticleTranslations.Last(); - // Assert.Null(orderLine2Article1ArticleTranslations2.LocaleId);// is part of PrimaryKey - // Assert.Equal("Artikel 2 nl-NL", orderLine2Article1ArticleTranslations2.Description); - // Assert.Equal(0, orderLine2Article1ArticleTranslations2.ArticleId); - //} } -} +} \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/SameInstancesForEntitiesWithSamePrimaryKeysTests.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/SameInstancesForEntitiesWithSamePrimaryKeysTests.cs new file mode 100644 index 0000000..0b1b95e --- /dev/null +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/SameInstancesForEntitiesWithSamePrimaryKeysTests.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EntityCloner.Microsoft.EntityFrameworkCore.Tests.TestBase; +using EntityCloner.Microsoft.EntityFrameworkCore.Tests.TestModels; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EntityCloner.Microsoft.EntityFrameworkCore.Tests +{ + public class SameInstancesForEntitiesWithSamePrimaryKeysTests : DbContextTestBase + { + private readonly Blog _blog; + private readonly Post _post1; + private readonly Post _post2; + private readonly Tag _tag1; + private readonly Tag _tag2; + private readonly Tag _tag3; + private readonly TagHeader _tagHeader1; + private readonly TagHeader _tagHeader2; + + public SameInstancesForEntitiesWithSamePrimaryKeysTests() : base(nameof(SameInstancesForEntitiesWithSamePrimaryKeysTests)) + { + _blog = new Blog + { + //Id = 1, + Name = "Blog1" + }; + + _tagHeader1 = new TagHeader + { + Header = "Tagheader1" + }; + + _tagHeader2 = new TagHeader + { + Header = "Tagheader2" + }; + + _tag1 = new Tag + { + //Id = 3, + Text = "tag1", + TagHeader = _tagHeader1, + TagHeaderId = _tagHeader1.Id, + }; + _tag2 = new Tag + { + //Id = 4, + Text = "tag2", + TagHeader = _tagHeader2, + TagHeaderId = _tagHeader2.Id, + }; + _tag3 = new Tag + { + //Id = 5, + Text = "tag3", + TagHeader = _tagHeader1, + TagHeaderId = _tagHeader1.Id, + }; + + _post1 = new Post + { + //Id = 6, + Content = "ContentPost1", + Title = "TitlePost1", + BlogId = _blog.Id, + Blog = _blog, + Tags = new List { _tag1, _tag2, _tag3 } + + }; + _blog.Posts.Add(_post1); + + _post2 = new Post + { + //Id = 7, + Content = "ContentPost1", + Title = "TitlePost1", + BlogId = _blog.Id, + Blog = _blog, + Tags = new List { _tag1, _tag2 } + + }; + _tag1.Posts.Add(_post1); + _tag1.Posts.Add(_post2); + _tag2.Posts.Add(_post1); + _tag2.Posts.Add(_post2); + + _blog.Posts.Add(_post1); + _blog.Posts.Add(_post2); + + _blog.FirstTag = _tag1; + + TestDbContext.Set().Add(_blog); + TestDbContext.SaveChanges(); + } + [Fact] + public async Task Blog_CloneWithIncludeOnTagsAndFirstTagBothShouldHaveSameInstanceButAlsoTagHeaderAsDeepestIncludedEntity() + { + // Arrange + var entity = await TestDbContext.Set() + .Include(b => b.FirstTag) //TagHeader not Included here + .Include(b => b.Posts) + .ThenInclude(b => b.Tags) + .ThenInclude(b => b.TagHeader)// Included here + .Where(c => c.Id == _blog.Id) + .AsNoTracking() + .SingleAsync(); + + // Act + var cloneBlog = await TestDbContext.CloneAsync(entity); + + var cloneFirstTag = cloneBlog.FirstTag; + var cloneTag = cloneBlog.Posts.First().Tags.First(); + + Assert.Same(cloneTag, cloneFirstTag); + Assert.NotNull(cloneTag.TagHeader); + Assert.NotNull(cloneFirstTag.TagHeader); // Should also have include TagHeader from other include + Assert.Same(cloneTag.TagHeader, cloneFirstTag.TagHeader); // Both TagHeaders should be same instance + + } + } +} + diff --git a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Blog.cs b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Blog.cs index a94bffe..c31584b 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Blog.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore.Tests/TestModels/Blog.cs @@ -11,5 +11,7 @@ public class Blog : IEntity public ICollection Posts { get; set; } = new List(); // Collection navigation public BlogAssets Assets { get; set; } // Reference navigation + public int? FirstTagId { get; set; } + public Tag FirstTag { get; set; } } } \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs b/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs index 3153726..575dc44 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs +++ b/EntityCloner.Microsoft.EntityFrameworkCore/DbContextExtensions.cs @@ -14,6 +14,7 @@ namespace EntityCloner.Microsoft.EntityFrameworkCore { public static class DbContextExtensions { + #region Cloning public static async Task> CloneAsync(this DbContext source, IQueryable queryable) where TEntity : class { @@ -87,6 +88,130 @@ public static async Task CloneAsync(this DbContext source, Fun return clonedEntity; } + private static object InternalClone(this DbContext source, object entity, string definingNavigationName, IReadOnlyEntityType definingEntityType, Dictionary references) + { + var primaryKeyStringOrInstance = source.CreatePrimaryKeyStringOrInstance(entity); + var isEarlierClonedEntity = references.ContainsKey(primaryKeyStringOrInstance); + + var jsonSettings = new JsonSerializerSettings + { + PreserveReferencesHandling = PreserveReferencesHandling.All, + TypeNameHandling = TypeNameHandling.Auto, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + + string jsonString = JsonConvert.SerializeObject(entity, jsonSettings); + object clonedEntity = JsonConvert.DeserializeObject(jsonString, entity.GetType(), jsonSettings); + + if (isEarlierClonedEntity) + { + var earlierClonedEntity = references[primaryKeyStringOrInstance]; + source.MergeNavigationProperties(earlierClonedEntity, clonedEntity, definingNavigationName, definingEntityType); + return earlierClonedEntity; + } + else + { + references.Add(primaryKeyStringOrInstance, clonedEntity); + } + + // source.CloneOwnedEntityProperties(entity, definingNavigationName, definingEntityType, references, clonedEntity); + + source.ResetEntityProperties(entity, definingNavigationName, definingEntityType, clonedEntity); + + source.ResetNavigationProperties(entity, definingNavigationName, definingEntityType, references, clonedEntity); + + return 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 IEnumerable InternalCloneCollection(this DbContext source, Dictionary references, Type collectionItemType, string definingNavigationName, IReadOnlyEntityType definingEntityType, IEnumerable collectionValue) + { + // https://learn.microsoft.com/en-us/ef/core/modeling/relationships/navigations#collection-types + // The underlying collection instance must be implement ICollection, and must have a working Add method. It is common to use List or HashSet + var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(collectionItemType)); + if (list == null) + { + throw new ArgumentNullException(nameof(collectionItemType)); + } + foreach (var item in collectionValue) + { + var clonedItemValue = source.InternalClone(item, definingNavigationName, definingEntityType, references); + list.Add(clonedItemValue); + } + + return (IEnumerable)ConvertToCollectionType(collectionValue.GetType(), collectionItemType, list); + } + + #endregion + + #region Helpers + private static IReadOnlyEntityType FindCurrentEntityType(this DbContext source, Type entityClrType, string definingNavigationName, IReadOnlyEntityType 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 object CreatePrimaryKeyStringOrInstance(this DbContext source, object entity) + { + var entityType = source.FindCurrentEntityType(entity.GetType(), null, null); + var primaryKeyProperties = entityType?.FindPrimaryKey()?.Properties.Select(p => p.PropertyInfo).ToList(); + if (primaryKeyProperties == null) + { + // no primary key, then use instance as key. + return entity; + } + + string primaryKeyString = null; + var jsonSettings = new JsonSerializerSettings + { + PreserveReferencesHandling = PreserveReferencesHandling.All, + TypeNameHandling = TypeNameHandling.Auto, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + }; + + for (int i = 0; i < primaryKeyProperties.Count; i++) + { + var primaryKeyProperty = primaryKeyProperties[i]; + var idPart = primaryKeyProperty.GetValue(entity); + + if (idPart?.GetType() != primaryKeyProperty.PropertyType) + { + throw new NotSupportedException( + $"CreatePrimaryKeyString only can handle id of type '{primaryKeyProperty.PropertyType.FullName}', passed id of type '{idPart?.GetType().FullName}'"); + } + + string separator = ""; + if (!string.IsNullOrWhiteSpace(primaryKeyString)) + { + separator = "-"; + } + + string idPartString = idPart.GetType() == typeof(string) ? idPart?.ToString() : JsonConvert.SerializeObject(idPart, jsonSettings); + primaryKeyString = primaryKeyString + separator + idPartString; + } + + return entityType.Name + "-" + primaryKeyString; + } + private static object ConvertToCollectionType(Type collectionType, Type entityType, ICollection collectionValue) { if (collectionType.IsInterface) @@ -166,45 +291,9 @@ private static Expression MakeBinary(ExpressionType type, Expression left, objec return Expression.MakeBinary(type, left, right); } - private static object InternalClone(this DbContext source, object entity, string definingNavigationName, IReadOnlyEntityType definingEntityType, Dictionary references) - { - if (references.ContainsKey(entity)) - { - return references[entity]; - } + #endregion - var jsonSettings = new JsonSerializerSettings - { - PreserveReferencesHandling = PreserveReferencesHandling.All, - TypeNameHandling = TypeNameHandling.Auto, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }; - - string jsonString = JsonConvert.SerializeObject(entity, jsonSettings); - object clonedEntity = JsonConvert.DeserializeObject(jsonString, entity.GetType(), jsonSettings); - - references.Add(entity, clonedEntity); - // source.CloneOwnedEntityProperties(entity, definingNavigationName, definingEntityType, references, clonedEntity); - - source.ResetEntityProperties(entity, definingNavigationName, definingEntityType, clonedEntity); - - source.ResetNavigationProperties(entity, definingNavigationName, definingEntityType, references, clonedEntity); - - return 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); - // } - // } - //} + #region Reset Properties private static void ResetNavigationProperties(this DbContext source, object entity, string definingNavigationName, IReadOnlyEntityType definingEntityType, Dictionary references, object clonedEntity) { @@ -216,8 +305,7 @@ private static void ResetNavigationProperties(this DbContext source, object enti ResetNavigationProperty(source, entity, references, clonedEntity, navigation); } - IEnumerable skipNavigations = entityType.GetSkipNavigations(); - foreach (var navigation in skipNavigations) + foreach (var navigation in entityType.GetSkipNavigations()) { ResetSkipNavigationProperty(source, entity, references, clonedEntity, navigation); } @@ -228,35 +316,30 @@ private static void ResetNavigationProperty(DbContext source, objec where TNavigation : IReadOnlyNavigation { var navigationValue = navigation.PropertyInfo?.GetValue(entity); - - if (navigation.IsOnDependent && navigationValue != null) + if (navigationValue != null) { - foreach (var foreignKeyProperty in navigation.ForeignKey.Properties) + if (navigation.IsOnDependent) { - ResetProperty(foreignKeyProperty, clonedEntity); + foreach (var foreignKeyProperty in navigation.ForeignKey.Properties) + { + ResetProperty(foreignKeyProperty, clonedEntity); + } } - } - - if (navigationValue != null) - { + if (navigation.IsCollection) { - //var collection = source.InternalCloneCollection(references, entity, navigation.ClrType.GenericTypeArguments[0], navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName, navigation.ForeignKey.DeclaringEntityType.DefiningEntityType, (IEnumerable)navigationValue); var collection = source.InternalCloneCollection(references, navigation.ClrType.GenericTypeArguments[0], navigation.Name, navigation.DeclaringEntityType, (IEnumerable)navigationValue); navigation.PropertyInfo.SetValue(clonedEntity, collection); } else { - //var clonedPropertyValue = source.InternalClone(navigationValue, navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName, navigation.ForeignKey.DeclaringEntityType.DefiningEntityType, references); var clonedPropertyValue = source.InternalClone(navigationValue, navigation.Name, navigation.DeclaringEntityType, references); navigation.PropertyInfo.SetValue(clonedEntity, clonedPropertyValue); } } } - - private static void ResetSkipNavigationProperty(DbContext source, object entity, - Dictionary references, object clonedEntity, TNavigation navigation) + private static void ResetSkipNavigationProperty(DbContext source, object entity, Dictionary references, object clonedEntity, TNavigation navigation) where TNavigation : IReadOnlySkipNavigation { var navigationValue = navigation.PropertyInfo?.GetValue(entity); @@ -272,37 +355,17 @@ private static void ResetSkipNavigationProperty(DbContext source, o if (navigation.IsCollection) { - //var collection = source.InternalCloneCollection(references, entity, navigation.ClrType.GenericTypeArguments[0], navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName, navigation.ForeignKey.DeclaringEntityType.DefiningEntityType, (IEnumerable)navigationValue); var collection = source.InternalCloneCollection(references, navigation.ClrType.GenericTypeArguments[0], navigation.Name, navigation.DeclaringEntityType, (IEnumerable)navigationValue); navigation.PropertyInfo.SetValue(clonedEntity, collection); } else { - //var clonedPropertyValue = source.InternalClone(navigationValue, navigation.ForeignKey.DeclaringEntityType.DefiningNavigationName, navigation.ForeignKey.DeclaringEntityType.DefiningEntityType, references); var clonedPropertyValue = source.InternalClone(navigationValue, navigation.Name, navigation.DeclaringEntityType, references); navigation.PropertyInfo.SetValue(clonedEntity, clonedPropertyValue); } } } - private static IEnumerable InternalCloneCollection(this DbContext source, Dictionary references, Type collectionItemType, string definingNavigationName, IReadOnlyEntityType definingEntityType, IEnumerable collectionValue) - { - // https://learn.microsoft.com/en-us/ef/core/modeling/relationships/navigations#collection-types - // The underlying collection instance must be implement ICollection, and must have a working Add method. It is common to use List or HashSet - var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(collectionItemType)); - if (list == null) - { - throw new ArgumentNullException(nameof(collectionItemType)); - } - foreach (var item in collectionValue) - { - var clonedItemValue = source.InternalClone(item, definingNavigationName, definingEntityType, references); - list.Add(clonedItemValue); - } - - return (IEnumerable)ConvertToCollectionType(collectionValue.GetType(), collectionItemType, list); - } - private static void ResetEntityProperties(this DbContext source, object entity, string definingNavigationName, IReadOnlyEntityType definingEntityType, object clonedEntity) { foreach (var property in source.FindCurrentEntityType(entity.GetType(), definingNavigationName, definingEntityType).GetProperties()) @@ -319,19 +382,6 @@ private static void ResetEntityProperties(this DbContext source, object entity, } } - private static IReadOnlyEntityType FindCurrentEntityType(this DbContext source, Type entityClrType, string definingNavigationName, IReadOnlyEntityType 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(IReadOnlyProperty property, object entity) { if (property.PropertyInfo == null) @@ -350,5 +400,81 @@ private static object GetDefault(Type type) } return null; } + + #endregion + + #region Merge properties + + private static void MergeNavigationProperties(this DbContext source, object toEntity, object fromEntity, string definingNavigationName, IReadOnlyEntityType definingEntityType) + { + var entityType = source.FindCurrentEntityType(toEntity.GetType(), definingNavigationName, definingEntityType); + if (entityType != null) + { + foreach (var navigation in entityType.GetNavigations()) + { + MergeNavigationProperty(source, toEntity, fromEntity, navigation); + } + + foreach (var navigation in entityType.GetSkipNavigations()) + { + MergeSkipNavigationProperty(source, toEntity, fromEntity, navigation); + } + } + } + + private static void MergeNavigationProperty(DbContext source, object toEntity, object fromEntity, TNavigation navigation) + where TNavigation : IReadOnlyNavigation + { + var toEntityNavigationValue = navigation.PropertyInfo?.GetValue(toEntity); + if (toEntityNavigationValue == null) + { + //if (navigation.IsOnDependent) + //{ + // foreach (var foreignKeyProperty in navigation.ForeignKey.Properties) + // { + // MergeProperty(foreignKeyProperty, toEntity, fromEntity); + // } + //} + + var fromEntityNavigationValue = navigation.PropertyInfo?.GetValue(fromEntity); + navigation.PropertyInfo.SetValue(toEntity, fromEntityNavigationValue); + } + } + + private static void MergeSkipNavigationProperty(DbContext source, object toEntity, object fromEntity, TNavigation navigation) + where TNavigation : IReadOnlySkipNavigation + { + var toEntityNavigationValue = navigation.PropertyInfo?.GetValue(toEntity); + if (toEntityNavigationValue == null) + { + //if (navigation.ForeignKey != null && navigation.IsOnDependent) + //{ + // foreach (var foreignKeyProperty in navigation.ForeignKey.Properties) + // { + // MergeProperty(foreignKeyProperty, toEntity, fromEntity); + // } + //} + + var fromEntityNavigationValue = navigation.PropertyInfo?.GetValue(fromEntity); + navigation.PropertyInfo.SetValue(toEntity, fromEntityNavigationValue); + } + } + + //private static void MergeProperty(IReadOnlyProperty property, object entity, object clonedEntity) + //{ + // if (property.PropertyInfo == null) + // { + // return; + // } + + // var firstValue = property.PropertyInfo.GetValue(entity); + // var secondValue = property.PropertyInfo.GetValue(clonedEntity); + // if (firstValue == null) + // { + // property.PropertyInfo.SetValue(entity, secondValue); + // } + //} + + #endregion } } \ No newline at end of file diff --git a/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj b/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj index d85434f..e1d93da 100644 --- a/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj +++ b/EntityCloner.Microsoft.EntityFrameworkCore/EntityCloner.Microsoft.EntityFrameworkCore.csproj @@ -12,7 +12,7 @@ Clone DeepClone Entity Entities Include ThenInclude Core EntityFramework EF Cloning entities using EntityFrameworkCore configuration Henk Kin - 10.0.0 + 10.1.0 true true true