Skip to content

[Bug]: CommandBatchPreparer.AddUniqueValueEdges fails to order UPDATE commands on unique index (Regression in 11.0 Preview 7) #38917

Description

@bkoelman

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 (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 ("Avoid false circular dependency edges for unchanged unique index values during update batching", fixing #35588) added checks to CommandBatchPreparer.AddUniqueValueEdges to skip edge creation for Modified entities when the unique index value is unchanged:

// 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 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

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<WorkItemGroup> Groups => Set<WorkItemGroup>();
    public DbSet<RgbColor> RgbColors => Set<RgbColor>();

    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<WorkItemGroup>()
            .HasOne(g => g.Color)
            .WithOne(c => c.Group)
            .HasForeignKey<RgbColor>("GroupId");
    }
}

Stack traces

PostgreSQL (Npgsql):

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):

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):

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions