Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
101 changes: 101 additions & 0 deletions test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IStateManager>();

// 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<DocumentEntity>(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<ContentEntity>(b =>
{
b.HasKey(c => c.Id);
b.Property(c => c.Data);
});

// Add FK relationship from Document to Content through FileContentId
modelBuilder.Entity<DocumentEntity>()
.HasOne<ContentEntity>()
.WithMany()
.HasForeignKey(d => d.FileContentId);

return modelBuilder.Model.FinalizeModel();
}
}