diff --git a/NuGet.config b/NuGet.config index b8e45392bc6..11bcc2d5e56 100644 --- a/NuGet.config +++ b/NuGet.config @@ -19,10 +19,6 @@ - - - - diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index d9d8dea9f5f..042e8744a7d 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -283,7 +283,7 @@ private bool LocalDetectChanges(InternalEntryBase entry) } } - foreach (var complexProperty in entry.StructuralType.GetComplexProperties()) + foreach (var complexProperty in entry.StructuralType.GetFlattenedComplexProperties()) { if (complexProperty.IsCollection) { diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs index f0089b8d870..ef361b1a753 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs @@ -674,6 +674,51 @@ public void GetEntry_throws_when_accessing_invalid_current_ordinal() Assert.Equal(CoreStrings.ComplexCollectionEntryOrdinalInvalid(5, "Blog", "Tags", 1), ex.Message); } + [ConditionalFact] + public void DetectChanges_detects_changes_in_nested_complex_collections() + { + var model = CreateModelWithNestedComplexCollections(); + var entityType = model.FindEntityType(typeof(BlogWithNested))!; + + var serviceProvider = InMemoryTestHelpers.Instance.CreateContextServices(model); + var stateManager = serviceProvider.GetRequiredService(); + var changeDetector = serviceProvider.GetRequiredService(); + + var blog = new BlogWithNested + { + NestedJson = new NestedJson + { + Item = new NestedItem { Name = "foo" }, + Items = + [ + new NestedItem { Name = "bar" }, + new NestedItem { Name = "baz" } + ] + } + }; + + var entityEntry = stateManager.GetOrCreateEntry(blog); + entityEntry.SetEntityState(EntityState.Unchanged); + + Assert.Equal(EntityState.Unchanged, entityEntry.EntityState); + + // Replace the NestedJson with a new instance that has a modified Items collection + blog.NestedJson = blog.NestedJson with + { + Items = + [ + new NestedItem { Name = "bar" }, + new NestedItem { Name = "baz" }, + new NestedItem { Name = "new-bar" } + ] + }; + + // DetectChanges should detect the change in the nested complex collection + changeDetector.DetectChanges(stateManager); + + Assert.Equal(EntityState.Modified, entityEntry.EntityState); + } + private static IModel CreateModel() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); @@ -688,6 +733,22 @@ private static IModel CreateModel() return modelBuilder.FinalizeModel(); } + private static IModel CreateModelWithNestedComplexCollections() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + + modelBuilder.Entity(eb => + { + eb.ComplexProperty(e => e.NestedJson, b => + { + b.ComplexProperty(a => a.Item); + b.ComplexCollection(a => a.Items); + }); + }); + + return modelBuilder.FinalizeModel(); + } + private class Blog { public int Id { get; set; } @@ -714,4 +775,22 @@ private class Category public string Name { get; set; } = ""; public string Description { get; set; } = ""; } + + private class BlogWithNested + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public NestedJson NestedJson { get; set; } = new(); + } + + private record NestedJson + { + public NestedItem Item { get; init; } = new(); + public List Items { get; init; } = []; + } + + private record NestedItem + { + public string Name { get; init; } = ""; + } }