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
112 changes: 112 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,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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This test passes even without the fix. Modify it so it fails without the fix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 7fd2d9c. The test now uses actual owned entities (FileEntity owned by DocumentEntity with an FK to ContentEntity) and properly fails without the fix. However, I discovered that the fix from PR #36769 doesn't make the test pass either, suggesting there may be an issue with the original fix or additional work needed to make it work correctly with the owned entity scenario.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Ok, then develop a different fix. Note that the original issue was reported at #36059

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Continue

{
// 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<IStateManager>();

// 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<DocumentEntity>(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<ContentEntity>()
.WithMany()
.HasForeignKey(f => f.ContentId);
});
});

modelBuilder.Entity<ContentEntity>(b =>
{
b.HasKey(c => c.Id);
b.Property(c => c.Data);
});

return modelBuilder.Model.FinalizeModel();
}
}
Loading