Skip to content
Merged
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
15 changes: 6 additions & 9 deletions src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1264,15 +1264,12 @@ private void AddUniqueValueEdges()
var rowIndexValueFactory = ((TableIndex)index).GetRowIndexValueFactory();
var (value, hasNullValue) = rowIndexValueFactory.CreateEquatableIndexValue(command);

if (command.EntityState == EntityState.Modified)
{
var (originalValue, _) = rowIndexValueFactory.CreateEquatableIndexValue(command, fromOriginalValues: true);
if (Equals(originalValue, value))
{
continue;
}
}

// Note: unlike the predecessor (first) pass above, we don't skip creating an edge here when the
// command's own index value appears unchanged. A predecessor is only ever registered in
// indexPredecessorsMap for a value it is actually releasing (see the check above), so if this
// command's value matches one, it genuinely needs to wait for that release, regardless of whether
// its own current/original value comparison (which can be unreliable, e.g. for attached entities
// whose original value was snapshotted after being modified) suggests no change occurred.
Comment thread
AndriySvyryd marked this conversation as resolved.
Resolved
if (value != null)
{
AddMatchingPredecessorEdge(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,77 @@ public class Blog
public string? Name { get; set; }
}

[Theory, MemberData(nameof(IsAsyncData))] // Issue #38917
public virtual async Task Reassigning_dependent_with_unique_FK_to_attached_entity_orders_updates_correctly(bool async)
{
var contextFactory = await InitializeNonSharedTest<DbContext>(
onModelCreating: mb => mb.Entity<WorkItemGroup38917>()
.HasOne(g => g.Color)
.WithOne(c => c.Group)
.HasForeignKey<RgbColor38917>("GroupId"),
seed: async context =>
{
var group0 = new WorkItemGroup38917 { Name = "G0" };
var group1 = new WorkItemGroup38917 { Name = "G1" };
var color0 = new RgbColor38917 { Id = "0x9a79e4", DisplayName = "red" };
var color1 = new RgbColor38917 { Id = "0xb4e6b9", DisplayName = "turquoise" };

group0.Color = color0;
group1.Color = color1;
context.AddRange(group0, group1);

await context.SaveChangesAsync();
});

await ExecuteWithStrategyInTransactionAsync(
contextFactory,
async context =>
{
var g1 = await context.Set<WorkItemGroup38917>()
.Include(g => g.Color)
.SingleAsync(g => g.Name == "G1");

var c0 = new RgbColor38917 { Id = "0x9a79e4" };
context.Attach(c0);
g1.Color = c0;

if (async)
{
await context.SaveChangesAsync();
}
else
{
context.SaveChanges();
}
},
async context =>
{
var g1 = await context.Set<WorkItemGroup38917>()
.Include(g => g.Color)
.SingleAsync(g => g.Name == "G1");
Assert.Equal("0x9a79e4", g1.Color!.Id);

var g0 = await context.Set<WorkItemGroup38917>()
.Include(g => g.Color)
.SingleAsync(g => g.Name == "G0");
Assert.Null(g0.Color);
});
}

public class WorkItemGroup38917
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = "";
public RgbColor38917? Color { get; set; }
}

public class RgbColor38917
{
public string Id { get; set; } = "";
public string? DisplayName { get; set; }
public WorkItemGroup38917? Group { get; set; }
}

[Theory, MemberData(nameof(IsAsyncData))]
public virtual async Task Update_entity_with_not_loaded_property_excludes_column_from_SQL(bool async)
{
Expand Down
47 changes: 47 additions & 0 deletions test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,40 @@ public void BatchCommands_skips_unique_index_edges_for_unchanged_store_generated
Assert.Equal(2, batch.ModificationCommands.Count);
}

[Fact] // Issue #38917
public void BatchCommands_orders_unique_index_release_before_reacquisition_with_unreliable_original_value()
{
var model = CreateModelWithUniqueIndexOnly();
var configuration = CreateContextServices(model);
var stateManager = configuration.GetRequiredService<IStateManager>();

// Releases "Test" by changing UniqueValue away from it. Uses a higher Id than the acquiring entry below so
// that the default (primary-key-based) command ordering alone would produce the wrong (unsafe) order,
// and only the dependency edge asserted by this test can produce the correct order.
var releasingEntry = stateManager.GetOrCreateEntry(
new FakeEntity { Id = 2, UniqueValue = "Other" });
releasingEntry.SetEntityState(EntityState.Modified);
releasingEntry.SetOriginalValue(releasingEntry.EntityType.FindProperty(nameof(FakeEntity.UniqueValue))!, "Test");

// Simulates an attached entity whose original value snapshot ends up matching its (newly assigned) current
// value; it still needs to wait for another command to release "Test" before it can claim it.
var acquiringEntry = stateManager.GetOrCreateEntry(
new FakeEntity { Id = 1, UniqueValue = "Test" });
acquiringEntry.SetEntityState(EntityState.Modified);
acquiringEntry.SetOriginalValue(acquiringEntry.EntityType.FindProperty(nameof(FakeEntity.UniqueValue))!, "Test");

var modelData = new UpdateAdapter(stateManager);

var batches = CreateBatches([acquiringEntry, releasingEntry], modelData);
var batch = Assert.Single(batches);

// The command releasing "Test" must be ordered before the command acquiring it, otherwise we'd get a
// unique constraint violation.
Assert.Equal(
[releasingEntry, acquiringEntry],
batch.ModificationCommands.Select(c => c.Entries.Single()));
}

[Fact]
public void BatchCommands_creates_valid_batch_for_shared_table_added_entities()
{
Expand Down Expand Up @@ -1063,6 +1097,19 @@ private static IModel CreateSimpleFKModel()
return modelBuilder.Model.FinalizeModel();
}

private static IModel CreateModelWithUniqueIndexOnly()
{
var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder();

modelBuilder.Entity<FakeEntity>(b =>
{
b.Ignore(c => c.RelatedId);
b.HasIndex(c => c.UniqueValue).IsUnique();
});

return modelBuilder.Model.FinalizeModel();
}

private static IModel CreateFKOneToManyModelWithGeneratedIds()
{
var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder();
Expand Down
Loading