dotnet / dotnet/efcore

SaveChangesAsync is slow due to cascade delete

Open
#34,718 1 comment 0 reactions 0 assignees View on GitHub
area-change-tracking area-perf customer-reported
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.