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..e36fc52aa09 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -1206,9 +1206,110 @@ 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 inline owned entity + var model = CreateOwnedEntityWithFKModel(); + var configuration = CreateContextServices(model); + var stateManager = configuration.GetRequiredService(); + + // Create original content and document + var originalContent = new ContentEntity { Id = 1, Data = "original data" }; + var document = new DocumentEntity { Id = 1, Name = "Test Doc", FileId = 1, FileName = "original.txt", FileContentId = 1 }; + + var originalContentEntry = stateManager.GetOrCreateEntry(originalContent); + originalContentEntry.SetEntityState(EntityState.Unchanged); + + var documentEntry = stateManager.GetOrCreateEntry(document); + documentEntry.SetEntityState(EntityState.Unchanged); + + // Now create new content + var newContent = new ContentEntity { Id = 2, Data = "new data" }; + var newContentEntry = stateManager.GetOrCreateEntry(newContent); + newContentEntry.SetEntityState(EntityState.Added); + + // Simulate replacing the owned entity by updating document properties to reference new content + document.FileId = 2; + document.FileName = "new.txt"; + document.FileContentId = 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, 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; } + // Owned File entity properties (flattened) + public int? FileId { get; set; } + public string FileName { get; set; } + public int? FileContentId { 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); + b.Property(d => d.FileId); + b.Property(d => d.FileName); + b.Property(d => d.FileContentId); + }); + + modelBuilder.Entity(b => + { + b.HasKey(c => c.Id); + b.Property(c => c.Data); + }); + + // Add FK relationship from Document to Content through FileContentId + modelBuilder.Entity() + .HasOne() + .WithMany() + .HasForeignKey(d => d.FileContentId); + + return modelBuilder.Model.FinalizeModel(); + } }