[Bug]: CommandBatchPreparer.AddUniqueValueEdges fails to order UPDATE commands on unique index (Regression in 11.0 Preview 7)
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
When reassigning a 1:1 relationship with a unique index (foreign key on dependent side using a shadow property) to an attached entity, `DbContext.SaveChangesAsync()` throws a unique constraint violation exception (`duplicate key value violates unique constraint "IX_..."`).
This worked as expected in **EF Core 9**, **EF Core 10**, and **EF Core 11.0.0-preview.6**, but fails in **EF Core 11.0.0-preview.7**.
This regression was introduced by PR [#38581](https://github.com/dotnet/efcore/pull/38581) (commit [fb2b999e9b6acc578cff47edec132044618b6028](https://github.com/dotnet/efcore/commit/fb2b999e9b6acc578cff47edec132044618b6028)).
This occurs across all relational providers: **PostgreSQL (`Npgsql`)**, **SQL Server (`Microsoft.EntityFrameworkCore.SqlServer`)**, and **SQLite (`Microsoft.EntityFrameworkCore.Sqlite`)**.
#### Root Cause Analysis
PR [#38581](https://github.com/dotnet/efcore/pull/38581) ("Avoid false circular dependency edges for unchanged unique index values during update batching", fixing [#35588](https://github.com/dotnet/efcore/issues/35588)) added checks to `CommandBatchPreparer.AddUniqueValueEdges` to skip edge creation for `Modified` entities when the unique index value is unchanged:
```csharp
// Pass 2: Adding predecessor edges to successor commands
if (command.EntityState == EntityState.Modified)
{
var (originalValue, _) = rowIndexValueFactory.CreateEquatableIndexValue(command, fromOriginalValues: true);
if (Equals(originalValue, value))
{
continue;
}
}
```
However, when an untracked entity `c0` is attached via `DbContext.Attach(c0)` and then assigned to a principal `g1.Color = c0`:
1. `c0`'s shadow foreign key / unique index column `GroupId` is updated to `G1` during navigation fixup, with `IsModified = true` and `IsWrite = true`. Because `c0` was attached, both its `OriginalValue` and `CurrentValue` evaluate to `G1`.
2. The entity previously associated with `G1` (`c1`) has `GroupId` cleared (`OriginalValue = G1`, `CurrentValue = NULL`).
3. In `CommandBatchPreparer.AddUniqueValueEdges`:
- **Pass 1 (Predecessors)**: `c1` (`OriginalValue = G1, CurrentValue = NULL`) is registered in `indexPredecessorsMap[G1] = [c1]`.
- **Pass 2 (Successors)**: When evaluating `c0` (`CurrentValue = G1`), EF Core should create a dependency edge `c1 -> c0` so `c1` releases `GroupId` before `c0` claims it.
- However, because `c0` has `originalValue (G1) == value (G1)`, the new check in PR [#38581](https://github.com/dotnet/efcore/pull/38581) executes `continue;` and **skips adding the dependency edge `c1 -> c0`**.
4. Without the dependency edge in `_modificationCommandGraph`, `c0` is executed before `c1` (`UPDATE RgbColors SET GroupId = 'G1' WHERE Id = '0x9a79e4'`), violating the unique constraint `IX_RgbColors_GroupId`.
### Your code
```csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
// Minimal runnable reproduction
// Packages:
// - Microsoft.EntityFrameworkCore (11.0.0-preview.7.*)
// - Microsoft.EntityFrameworkCore.SqlServer (11.0.0-preview.7.*) OR
// - Microsoft.EntityFrameworkCore.Sqlite (11.0.0-preview.7.*) OR
// - Npgsql.EntityFrameworkCore.PostgreSQL (11.0.0-preview.6/preview.7)
var provider = Provider.Sqlite; // Change to Provider.SqlServer or Provider.Postgres to test other databases
await using var dbContext = new AppDbContext(provider);
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();
// 1. Seed: Group0 has Color0, Group1 has Color1
var group0 = new WorkItemGroup { Name = "G0" };
var group1 = new WorkItemGroup { Name = "G1" };
var color0 = new RgbColor { Id = "0x9a79e4", DisplayName = "red" };
var color1 = new RgbColor { Id = "0xb4e6b9", DisplayName = "turquoise" };
group0.Color = color0;
group1.Color = color1;
dbContext.Groups.AddRange(group0, group1);
await dbContext.SaveChangesAsync();
// 2. In a new context: reassign Group1's color to Color0
await using var updateContext = new AppDbContext(provider);
var g1 = await updateContext.Groups.Include(g => g.Color).FirstAsync(g => g.Name == "G1");
var c0 = new RgbColor { Id = "0x9a79e4" };
updateContext.Attach(c0);
g1.Color = c0;
// In EF Core 10 and EF Core 11.0.0-preview.6:
// Color1 (SET GroupId = NULL) executes BEFORE Color0 (SET GroupId = G1) -> Succeeds!
//
// In EF Core 11.0.0-preview.7:
// Color0 (SET GroupId = G1) executes BEFORE Color1 (SET GroupId = NULL) -> Throws DbUpdateException
await updateContext.SaveChangesAsync();
Console.WriteLine("Saved successfully!");
public class WorkItemGroup
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = "";
public RgbColor? Color { get; set; }
}
public class RgbColor
{
public string Id { get; set; } = "";
public string? DisplayName { get; set; }
public WorkItemGroup? Group { get; set; }
}
public enum Provider
{
Postgres,
SqlServer,
Sqlite
}
public class AppDbContext : DbContext
{
private readonly Provider _provider;
public AppDbContext(Provider provider)
{
_provider = provider;
}
public DbSet Groups => Set();
public DbSet RgbColors => Set();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
switch (_provider)
{
case Provider.Sqlite:
optionsBuilder.UseSqlite("Data Source=test_efcore_repro.db");
break;
case Provider.SqlServer:
optionsBuilder.UseSqlServer("Server=localhost,1433;Database=test_efcore_repro;User Id=sa;Password=Passw0rd!;TrustServerCertificate=True");
break;
case Provider.Postgres:
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=test_efcore_repro;Username=postgres;Password=postgres");
break;
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasOne(g => g.Color)
.WithOne(c => c.Group)
.HasForeignKey("GroupId");
}
}
```
### Stack traces
**PostgreSQL (`Npgsql`):**
```text
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
---> Npgsql.PostgresException (0x80004005): 23505: duplicate key value violates unique constraint "IX_RgbColors_GroupId"
DETAIL: Key ("GroupId")=(01a06c7d-c9bf-7a22-af83-dac4c89d178f) already exists.
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
```
**SQL Server (`Microsoft.EntityFrameworkCore.SqlServer`):**
```text
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert duplicate key row in object 'dbo.RgbColors' with unique index 'IX_RgbColors_GroupId'. The duplicate key value is (c5636c6b-c4ad-4297-94bf-fbbc681dff64).
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
```
**SQLite (`Microsoft.EntityFrameworkCore.Sqlite`):**
```text
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'UNIQUE constraint failed: RgbColors.GroupId'.
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
```
### EF Core version
`11.0.0-preview.7.*` (worked in `11.0.0-preview.6`, `10.0.x`, and `9.0.x`)
### Database provider
`Microsoft.EntityFrameworkCore.SqlServer` / `Microsoft.EntityFrameworkCore.Sqlite` / `Npgsql.EntityFrameworkCore.PostgreSQL`
### Target framework
`.NET 11.0`
### Operating system
`Windows 11`
### IDE
`Visual Studio 2026 / VS Code / Rider`
Contributor guide
Assessment
This issue has not been assessed yet.