Does it work?

Entity Framework Code First: Always disable AutoDetectChanges when importing data

This post dates back to 2013 and it is migrated from my old blog.

In this article I will show you that, when importing data with Entity Framework, you will almost always have performance issues unless you disable AutoDetectChanges.

When creating an application to be scheduled on a timely basis to import some data into the database you will end up having a portion of your code creating many Entities and adding them to the Context.
After having added all the new Entities you will invoke: SaveChanges.

In my example I suppose that you already used Entity Framework Code First.
I explicitly invoke a DatabaseInitializer to be sure to create a freshly new database.
I previously created a DbContext with a single Dbset of Customer entities:

public class CustomerContext : DbContext
{
  public DbSet<Customer> Customers { get; set; }
}

and I created the Customer entity:

public class Customer
{
  public long Id { get; set; }

  public string Name { get; set; }

  public string Surname { get; set; }
}

I created a Console application to simulate a massive import.
Not so massive, after all: only 1000 Customers.
I created a method adding the Customers to the DbSet and I invoked it twice: the first time I disable AutoDetectChanges before invoking it, the second time I enable AutoDetectChanges before invoking it.
The method contains a Stopwatch to track the time it takes to add the entities to the context and then to execute SaveChanges.

private static void MainMethod()
{
  try
  {
    using (var context = new CustomerContext())
    {
      var initializer = new DropCreateDatabaseAlways<CustomerContext>();
      initializer.InitializeDatabase(context);
      
      context.Configuration.AutoDetectChangesEnabled = true;
      Console.WriteLine("AutoDetectChangesEnabled = true");
      ImportData(context);
      context.Configuration.AutoDetectChangesEnabled = false;
      Console.WriteLine("AutoDetectChangesEnabled = false");
      ImportData(context);
    }
  }
  catch (Exception exception)
  {
    Console.WriteLine(exception.ToString());
  }

}

private static void ImportData(CustomerContext context)
{
  Stopwatch timer = new Stopwatch();
  timer.Start();
  for (int i = 0; i < 1000; i++)
  {
    var customer = new Customer { Name = "Test Customer"};
    context.Customers.Add(customer);
  }
  timer.Stop();
  Console.WriteLine("Items added: {0}", timer.Elapsed);
  timer.Restart();
  context.SaveChanges();
  timer.Stop();
  Console.WriteLine("SaveChanges ended: {0}", timer.Elapsed);
}

 

This is the result of my simple profiling:

AutoDetectChangesEnabled = true
Items added: 00:00:02.0399483
Items saved: 00:00:01.5622158
AutoDetectChangesEnabled = false
Items added: 00:00:00.0682787
Items saved: 00:00:01.1665518
Press any key to continue . . .

 

As you can see, when disabling AutoDetectChanges we obtain a relevant gain in performance:
0.068 seconds against 2.03 seconds.
It’s 2 orders of magnitude and there’s no significant change in the duration of the SaveChanges invocation.

You can try by yourself increasing the number of iterations and using your own model.

My model is a very small one and my test environment is not representative of a real
production environment but its goal is to show how relevant is the difference when
disabling AutoDetectChanges.

Such performance loss with AutoDetectChanges happens because, if AutoDetectChanges is enabled, every time we add an entity to the DbContext, the DetectChanges() method is invoked.
If we disable AutoDetectChanges instead, the DetectChanges() method is invoked only once: during SaveChanges().
I profiled a real application with Ants Profiler and I pinpointed the absolute predominance of the time spent invoking DetectChanges() above all other time consuming methods.

It’s important to notice that in my example I added to the context a single entity and not an object tree.
Let’s try with an object tree: add a CashAccount entity and add the relative property to Customer:

public class CashAccount
{
  public long Id { get; set; }

  public string Code { get; set; }

  public decimal Balance { get; set; }
}

public class Customer
{
  public long Id { get; set; }

  public string Name { get; set; }

  public string Surname { get; set; }

  public CashAccount CashAccount { get; set; }
}

Let’s modify the ImportData method to assign a CashAccount to each Customer.

private static void ImportData(CustomerContext context)
{
  Stopwatch timer = new Stopwatch();
  timer.Start();
  for (int i = 0; i < 1000; i++)
  {
    var customer = new Customer 
           { 
             Name = "Test Customer",
             CashAccount = cashAccount
           };                
    context.Customers.Add(customer);
  }
  timer.Stop();
  Console.WriteLine("Items added: {0}", timer.Elapsed);
  timer.Restart();
  context.SaveChanges();
  timer.Stop();
  Console.WriteLine("SaveChanges ended: {0}", timer.Elapsed);
}

If you run again the Console application and execute a couple of queries in your database you will find 2000 Customers and 2000 CashAccounts.
What we did here is to assign a CashAccount to the Customer and then add the Customer to the DbContext and everything works as expected.
Be careful at the following piece of code, now:

private static void ImportData(CustomerContext context)
{
  Stopwatch timer = new Stopwatch();
  timer.Start();
  for (int i = 0; i < 1000; i++)
  {
    var customer = new Customer { Name = "Test Customer"};
    context.Customers.Add(customer);
    customer.CashAccount = cashAccount;
  }
  timer.Stop();
  Console.WriteLine("Items added: {0}", timer.Elapsed);
  timer.Restart();
  context.SaveChanges();
  timer.Stop();
  Console.WriteLine("SaveChanges ended: {0}", timer.Elapsed);
}

If you run the Console application now you will find in your Database 2000 Customers but only 1000 CashAccounts.
In fact what we did here is to assign a CashAccount to the Customer after having added the Customer to the DbContext.
During the first invocation of the method: when AutoDetectChanges is enabled, the Context is aware of the CashAccount associated to the Customer after having added the Customer to the context.
During the second invocation: when AutoDetectChanges is disabled, the Context didn’t detect the change consisting in assigning a CashAccount to the Customer, so ImportData in this case will result in 1000 Customers saved to the Database and no CashAccounts.
If you want the association to the CashAccounts to be detected you need to re-enable AutoDetectChanges before invoking SaveChanges() like in the following example that modifies the previous one:

private static void ImportData(CustomerContext context)
{
  Stopwatch timer = new Stopwatch();
  timer.Start();
  for (int i = 0; i < 1000; i++)
  {
    var customer = new Customer { Name = "Test Customer"};
    context.Customers.Add(customer);
    customer.CashAccount = cashAccount;
  }
  timer.Stop();
  Console.WriteLine("Items added: {0}", timer.Elapsed);
  timer.Restart();
    context.Configuration.AutoDetectChangesEnabled = true;
  context.SaveChanges();
  timer.Stop();
  Console.WriteLine("SaveChanges ended: {0}", timer.Elapsed);
}

 

If you run the Console application now you will find again in your database 2000 Customers and 2000 CashAccounts.

Leave a Reply

%d bloggers like this: