Skip to content

Exception while using unique clustering key #35588

Open
@Morasiu

Description

Bug description

While saving collection, which has separate Clustering key and a composite made with Guid and enum I get a circular dependency exception.

Saving is simple and look like this:

testFromDb.Categories = new List<TestCategory>()
{
    new TestCategory()
    {
        Category = Category.Basic,
    },
    new TestCategory
    {
        Category = Category.Pro,
    }
};

dbContext.SaveChanges(); // Exception here

Tips which my team discovered:

  • if Category is type of string it works (not sure about it)
  • if modelBuilder.Entity<TestCategory>().HasIndex(x => x.ClusteringKey).IsUnique().IsClustered(); is done WITHOUT .IsUnique() it works
  • only fails if there is more than one category updated

Here is a full repo with docker-compose file, migrations and data seeding for you to easily debug this error :)

EFCoreCompositeKeyClustering.zip

Your code

using Microsoft.EntityFrameworkCore;

// Use `docker compose up -d` to run the SQL Server container

var dbContext = new ApplicationDbContext();
dbContext.Database.EnsureCreated();

// BUG BELOW

var testFromDb = dbContext.Tests.Include(x => x.Categories).First();
testFromDb.Description = "New description";
testFromDb.Categories.Clear();
testFromDb.Categories = new List<TestCategory>()
{
    new TestCategory()
    {
        Category = Category.Basic,
    },
    new TestCategory
    {
        Category = Category.Pro,
    }
};

dbContext.SaveChanges(); // Exception here

Console.WriteLine("IT WORKS!");
// BUG END (Not the village from Hobbit)

public class Test
{
    public Guid Id { get; set; }
    public string Description { get; set; }
    public int ClusteringKey { get; }
    public ICollection<TestCategory> Categories { get; set; } = [];
}

public class TestCategory
{
    public Guid TestId { get; set; }
    public Category Category { get; set; }
    public int ClusteringKey { get; }
}

public enum Category
{
    Basic,
    Pro,
    SuperPro
}

class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("Server=tcp:127.0.0.1,1433;Initial Catalog=TestDb;Persist Security Info=False;User ID=sa;Password=P@ssword1234;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;");
    }

    public DbSet<Test> Tests => Set<Test>();
    public DbSet<TestCategory> TestCategories => Set<TestCategory>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Test>()
            .HasKey(x => x.Id)
            .IsClustered(false);
         modelBuilder.Entity<Test>()
             .Property(x => x.ClusteringKey)
             .ValueGeneratedOnAdd()
             .UseIdentityColumn(1_000_000);
         modelBuilder.Entity<Test>()
             .HasIndex(x => x.ClusteringKey).IsUnique().IsClustered();
        modelBuilder.Entity<TestCategory>()
            .HasKey(x => new { x.TestId, x.Category })
            .IsClustered(false);
        modelBuilder.Entity<TestCategory>()
            .HasOne<Test>()
            .WithMany(x => x.Categories)
            .HasForeignKey(x => x.TestId)
            .OnDelete(DeleteBehavior.Cascade);
        modelBuilder.Entity<TestCategory>()
            .Property(x => x.ClusteringKey)
            .ValueGeneratedOnAdd()
            .UseIdentityColumn(1_000_000);
        modelBuilder.Entity<TestCategory>()
            .HasIndex(x => x.ClusteringKey).IsUnique().IsClustered(); // Works without IsUnique
        
        // SEED FOR TESTING
        var testGuid = Guid.NewGuid();
        modelBuilder.Entity<Test>()
            .HasData(new Test
            {
                Id = testGuid,
                Description = "Test",
            });
        modelBuilder.Entity<TestCategory>()
            .HasData(
            [
                new TestCategory
                {
                    Category = Category.Basic,
                    TestId = testGuid,
                },
                new TestCategory
                {
                    Category = Category.Pro,
                    TestId = testGuid,
                },
                new TestCategory
                {
                    Category = Category.SuperPro,
                    TestId = testGuid,
                }
            ]);
    }
}

Stack traces

Unhandled exception. System.InvalidOperationException: Unable to save changes because a circular dependency was detected in the data to be saved: 'TestCategory [Added] <-
Index { 'ClusteringKey' } TestCategory [Added] <-
Index { 'ClusteringKey' } TestCategory [Added]To show additional information call 'DbContextOptionsBuilder.EnableSensitiveDataLogging'.'.
   at Microsoft.EntityFrameworkCore.Utilities.Multigraph`2.ThrowCycle(List`1 cycle, Func`2 formatCycle, Func`2 formatException)
   at Microsoft.EntityFrameworkCore.Utilities.Multigraph`2.TopologicalSortCore(Boolean withBatching, Func`4 canBreakEdges, Func`2 formatCycle, Func`2 formatException)
   at Microsoft.EntityFrameworkCore.Utilities.Multigraph`2.BatchingTopologicalSort(Func`4 canBreakEdges, Func`2 formatCycle, Func`2 formatException)
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.TopologicalSort(IEnumerable`1 commands)
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.BatchCommands(IList`1 entries, IUpdateAdapter updateAdapter)+MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__112_0(DbContext _, ValueTuple`2 t)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
   at Program.<Main>$(String[] args) in D:\Projekty\EFCoreCompositeKeyClustering\Program.cs:line 25

Verbose output


EF Core version

8.0.6

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.Net 8.0

Operating system

Windows 11

IDE

Rider 2024.3.4

Metadata

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions