diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 6590593a40e..08690f863dc 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -881,7 +881,13 @@ private static bool CanCreateDependency(IForeignKey foreignKey, IReadOnlyModific return false; } - if (foreignKey.GetMappedConstraints().Any(c => (principal ? c.PrincipalTable : c.Table) == command.Table)) + // Special case: For owned entities that have FK relationships to other entities, + // we need to ensure dependencies are created even if the FK constraint exists. + // This is needed to fix FK dependency ordering when replacing owned entities. + var isOwnedEntityFKToNonOwner = foreignKey.DeclaringEntityType.IsOwned() + && foreignKey.PrincipalEntityType != foreignKey.DeclaringEntityType.FindOwnership()?.PrincipalEntityType; + + if (!isOwnedEntityFKToNonOwner && foreignKey.GetMappedConstraints().Any(c => (principal ? c.PrincipalTable : c.Table) == command.Table)) { // Handled elsewhere return false; diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index c02cf09ca18..f63af065346 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -1206,9 +1206,121 @@ public void BatchCommands_handles_null_values_when_sensitive_logging_enabled() Assert.DoesNotContain("Object reference not set", exception.Message); } + [ConditionalFact] + public void BatchCommands_sorts_FK_dependencies_correctly_when_replacing_owned_entity() + { + // Reproduces issue #36059: FK dependency ordering wrong when replacing an owned entity with FK to non-owner + var model = CreateOwnedEntityWithFKModel(); + var configuration = CreateContextServices(model); + var stateManager = configuration.GetRequiredService(); + + // Create original content + var originalContent = new ContentEntity { Id = 1, Data = "original data" }; + var originalContentEntry = stateManager.GetOrCreateEntry(originalContent); + originalContentEntry.SetEntityState(EntityState.Unchanged); + + // Create document with owned file that references original content + var document = new DocumentEntity + { + Id = 1, + Name = "Test Doc", + File = new FileEntity { Id = 1, FileName = "original.txt", ContentId = 1 } + }; + var documentEntry = stateManager.GetOrCreateEntry(document); + documentEntry.SetEntityState(EntityState.Unchanged); + + // Create new content + var newContent = new ContentEntity { Id = 2, Data = "new data" }; + var newContentEntry = stateManager.GetOrCreateEntry(newContent); + newContentEntry.SetEntityState(EntityState.Added); + + // Replace the owned file - manually mark document as modified + document.File = new FileEntity { Id = 2, FileName = "new.txt", ContentId = 2 }; + documentEntry.SetEntityState(EntityState.Modified); + + // Delete the original content + originalContentEntry.SetEntityState(EntityState.Deleted); + + var modelData = new UpdateAdapter(stateManager); + var batches = CreateBatches([originalContentEntry, newContentEntry, documentEntry], modelData); + var commands = batches.SelectMany(b => b.ModificationCommands).ToList(); + + // Find the commands + var insertContentCmd = commands.FirstOrDefault(c => c.EntityState == EntityState.Added && c.TableName == "ContentEntity"); + var updateDocumentCmd = commands.FirstOrDefault(c => c.EntityState == EntityState.Modified && c.TableName == "DocumentEntity"); + var deleteContentCmd = commands.FirstOrDefault(c => c.EntityState == EntityState.Deleted && c.TableName == "ContentEntity"); + + Assert.NotNull(insertContentCmd); + Assert.NotNull(updateDocumentCmd); + Assert.NotNull(deleteContentCmd); + + var insertIndex = commands.IndexOf(insertContentCmd); + var updateIndex = commands.IndexOf(updateDocumentCmd); + var deleteIndex = commands.IndexOf(deleteContentCmd); + + // The correct order should be: INSERT new content, UPDATE document (with new file), DELETE old content + // This ensures the FK constraint is not violated + Assert.True(insertIndex < updateIndex, + $"INSERT Content should come before UPDATE Document, but got INSERT at {insertIndex} and UPDATE at {updateIndex}"); + Assert.True(updateIndex < deleteIndex, + $"UPDATE Document should come before DELETE Content, but got UPDATE at {updateIndex} and DELETE at {deleteIndex}"); + } + private class AnotherFakeEntity { public int Id { get; set; } public int? AnotherId { get; set; } } + + private class DocumentEntity + { + public int Id { get; set; } + public string Name { get; set; } + public FileEntity File { get; set; } + } + + private class FileEntity + { + public int Id { get; set; } + public string FileName { get; set; } + public int? ContentId { get; set; } + } + + private class ContentEntity + { + public int Id { get; set; } + public string Data { get; set; } + } + + private static IModel CreateOwnedEntityWithFKModel() + { + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); + + modelBuilder.Entity(b => + { + b.HasKey(d => d.Id); + b.Property(d => d.Name); + + // Configure File as owned entity + b.OwnsOne(d => d.File, fb => + { + fb.Property(f => f.Id); + fb.Property(f => f.FileName); + fb.Property(f => f.ContentId); + + // Add FK from owned File to non-owned Content + fb.HasOne() + .WithMany() + .HasForeignKey(f => f.ContentId); + }); + }); + + modelBuilder.Entity(b => + { + b.HasKey(c => c.Id); + b.Property(c => c.Data); + }); + + return modelBuilder.Model.FinalizeModel(); + } }