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.
Suppose we have the following model of BankAccount
s and Deposit
s - a simple one-to-many relationship: BankAccount
has a collection of Deposit
s 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:
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:
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:"
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With