Open
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