Question

I recently tweaked part of my application that was running very slowly by disabling automatic change detection (Context.Configuration.AutoDetectChangesEnabled = false) before doing a bulk delete, then re-enabling it and saving the changes.

I read a couple different sources explaining that, essentially, whenever I call methods like .Add() or .Remove() on a DbSet, the DetectChanges() is being called, and that can get expensive when we're dealing with lots of entities. OK.

Now I want to draw attention to these articles in particular:

Entity Framework Automatic Detect Changes (MSDN)

An alternative to disabling and re-enabling is to leave automatic detection of changes turned off at all times and either call context.ChangeTracker.DetectChanges explicitly or use change tracking proxies diligently. Both of these options are advanced and can easily introduce subtle bugs into your application so use them with care.

Secrets of Detect Changes: Part 3

Don’t turn off automatic DetectChanges unless you really need to; it will just cause you pain.

Perhaps it's in front of me, but assuming that, for instance, I wrapped .SaveChanges() in a method that always called DetectChanges() first, what bugs could I start encountering that I wouldn't normally? All the warnings I can see just vaguely suggest that bad things can happen without going into what they are.

Was it helpful?

Solution

Suppose we have the following model of BankAccounts and Deposits - a simple one-to-many relationship: BankAccount has a collection of Deposits and a Deposit belongs to a single BankAccount:

public class BankAccount
{
    public int Id { get; set; }
    public int AccountNumber { get; set; }
    public string Owner { get; set; }
    public ICollection<Deposit> Deposits { get; set; }
}

public class Deposit
{
    public int Id { get; set; }
    public decimal Value { get; set; }

    public int BankAccountId { get; set; }
    public BankAccount BankAccount { get; set; }
}

And a simple database context:

public class MyContext : DbContext
{
    public DbSet<BankAccount> BankAccounts { get; set; }
    public DbSet<Deposit> Deposits { get; set; }
}

Mr. John Smith wants to have two accounts at our bank and pays a deposit of 1.000.000 $ to his first account. Our bank's programmer fulfills this task like so:

using (var ctx = new MyContext())
{
    var bankAccount123 = new BankAccount
    {
        AccountNumber = 123,
        Owner = "John Smith",
        Deposits = new List<Deposit> { new Deposit { Value = 1000000m } }
    };
    var bankAccount456 = new BankAccount
    {
        AccountNumber = 456,
        Owner = "John Smith"
    };

    ctx.BankAccounts.Add(bankAccount123);
    ctx.BankAccounts.Add(bankAccount456);

    ctx.SaveChanges();
}

And it works like expected:

DetectChanges 1

One day later, Mr. Smith calls the bank: "I changed my mind. I don't want those two accounts, only one, the one with account number 456, I like this number better. On my account 123 are 1 million dollar. Please move those to account 456 and then delete my account 123!"

Our programmer had heard that deleting is a dangerous thing and decided to copy the database into the test environment and to test first a new routine he writes now in order to follow Mr. Smith's request:

using (var ctx = new MyContext())
{
    var bankAccount123 = ctx.BankAccounts.Include(b => b.Deposits)
        .Single(b => b.AccountNumber == 123);
    var bankAccount456 = ctx.BankAccounts
        .Single(b => b.AccountNumber == 456);
    var deposit = bankAccount123.Deposits.Single();

    // here our programmer moves the deposit to account 456 by changing
    // the deposit's account foreign key
    deposit.BankAccountId = bankAccount456.Id;

    // account 123 is now empty and can be deleted safely, he thinks!
    ctx.BankAccounts.Remove(bankAccount123);

    ctx.SaveChanges();
}

He runs the test and it works:

DetectChanges 2

Before moving the code into production he decides to add a little performance improvement, but - of course - doesn't change the tested logic to move the deposit and to delete the account:

using (var ctx = new MyContext())
{
    // he added this well-known line to get better performance!
    ctx.Configuration.AutoDetectChangesEnabled = false;

    var bankAccount123 = ctx.BankAccounts.Include(b => b.Deposits)
        .Single(b => b.AccountNumber == 123);
    var bankAccount456 = ctx.BankAccounts
        .Single(b => b.AccountNumber == 456);
    var deposit = bankAccount123.Deposits.Single();

    deposit.BankAccountId = bankAccount456.Id;

    ctx.BankAccounts.Remove(bankAccount123);

    // he heard this line would be required when AutoDetectChanges is disabled!
    ctx.ChangeTracker.DetectChanges();
    ctx.SaveChanges();
}

He runs the code in production before he finishes his daily work.

Next day, Mr. Smith calls the bank: "I need half a million from my account 456!" The clerk at customer service says: "Sorry Sir, but there is no money on your account 456." Mr. Smith: "Ah OK, they haven't moved the money yet. Then, please, take the money from my account 123!" "Sorry Sir, but you don't have an account 123!" Mr. Smith: "WHAT???" Customer service: "I can see all your accounts and deposits in my banking tool and there is nothing on your single account 456:"

DetectChanges 3


What went wrong when our programmer added his little performance improvement and made Mr. Smith a poor man?

The important line that behaves different after setting AutoDetectChangesEnabled to false is ctx.BankAccounts.Remove(bankAccount123);. This line now doesn't call DetectChanges internally anymore. The result is that EF doesn't get knowledge about the change of the foreign key BankAccountId in the deposit entity (which happened before the call to Remove).

With enabled change detection Remove would have adjusted the whole object graph according to the changed foreign key ("relationship fixup"), i.e. deposit.BankAccount would have been set to bankAccount456, the deposit would have been removed from the bankAccount123.Deposits collection and added to the bankAccount456.Deposits collection.

Because that didn't happen Remove marked the parent bankAccount123 as Deleted and put the deposit - that is still a child in the bankAccount123.Deposits collection - into state Deleted as well. When SaveChanges is called both are deleted from the database.

Although this example looks a bit artificial I remember that I had similar "bugs" after disabling change detection in real code that took some time to find and understand. The main problem is that code that works and is tested with change detection possibly doesn't work anymore and needs to be tested again after change detection is disabled even though nothing was changed with that code. And perhaps the code must be modified to make it work correctly again. (In our example the programmer had to add ctx.ChangeTracker.DetectChanges(); before the Remove line to fix the bug.)

This is one of the possible "subtle bugs" the MSDN page is talking about. There are probably many more.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top