SaveChangesAsync is slow due to cascade delete
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
We have an entity hierarchy with a root entity that has several collections of dependent entities (the collections are big enough), some of the dependents may have their own dependent entities, and so on.
When some entities are removed from the root's collections, the call of `SaveChangesAsync` is extremely slow. It seems that the slowdown is caused by [change detection](https://github.com/dotnet/efcore/blob/b2e366bf1d1955903c75c06288bad5bc5b8deda0/src/EFCore/ChangeTracking/Internal/StateManager.cs#L1224) during cascade delete (because all parents and their navigation collections are checked). But it's possible to disable auto change detection, manually call `DetectChanges` before `SaveChangesAsync` and drastically increase performance:
```c#
db.ChangeTracker.AutoDetectChangesEnabled = false;
db.ChangeTracker.DetectChanges();
await db.SaveChangesAsync();
```
### Steps to reproduce
```c#
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
var db = new MainDbContext();
var first = new Root
{
Id = 1,
Children1 = Enumerable
.Range(1, 10)
.Select(i => new Child1
{
Id = i,
RootId = 1,
Children = Enumerable
.Range(1, 2000)
.Select(j => new Child11
{
Id = (i - 1) * 2000 + j,
Child1Id = i
})
.ToList()
})
.ToList(),
Children2 = Enumerable.Range(1, 30000).Select(i => new Child2 { Id = i, RootId = 1 }).ToList()
};
db.Add(first);
await db.SaveChangesAsync();
var watch = Stopwatch.StartNew();
first.Children1.RemoveAt(first.Children1.Count - 1);
// db.ChangeTracker.AutoDetectChangesEnabled = false;
// db.ChangeTracker.DetectChanges();
await db.SaveChangesAsync();
Console.WriteLine("{0}", watch.Elapsed);
internal class Root
{
public int Id { get; init; }
public List Children1 { get; init; } = [];
public List Children2 { get; init; } = [];
}
internal class Child1
{
public int Id { get; init; }
public int RootId { get; init; }
public List Children { get; init; } = [];
}
internal class Child11
{
public int Id { get; init; }
public int Child1Id { get; init; }
}
internal class Child2
{
public int Id { get; init; }
public int RootId { get; init; }
}
internal class MainDbContext : DbContext
{
public DbSet Root { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase("CascadeDeleteTest");
}
}
```
### Version
EF Core: 8.0.8
Target framework: net8.0
Operating system: Ubuntu 24.04
Contributor guide
Assessment
This issue has not been assessed yet.