What You Need To Know About EF Core Bulk Updates
Tags: efcore, dotnet, csharp, aspnetcore
Published: 2024-06-24 10:18:22

---
title: What You Need To Know About EF Core Bulk Updates
published: true
date: 2024-06-22 00:00:00 UTC
tags: efcore,dotnet,csharp,aspnetcore
canonical_url: https://www.milanjovanovic.tech/blog/what-you-need-to-know-about-ef-core-bulk-updates
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ef12yf3vc5ok6wp6zh1m.png
---
When you're dealing with thousands or even millions of records, efficiency is king. That's where [**EF Core bulk update**](https://www.milanjovanovic.tech/blog/how-to-use-the-new-bulk-update-feature-in-ef-core-7) capabilities come into play.

EF Core 7 introduced two powerful new methods, `ExecuteUpdate` and `ExecuteDelete`. They're designed to simplify bulk updates in your database. Both methods have their respective async overloads - `ExecuteUpdateAsync` and `ExecuteDeleteAsync`.[EF bulk updates](https://learn.microsoft.com/en-us/ef/core/saving/execute-insert-update-delete) offer significant performance advantages over traditional approaches.

However, there's an **important caveat**: these bulk operations bypass the EF Core **Change Tracker**. This disconnect can lead to unexpected behavior if you're not aware of it.

In this week's issue, we'll dive into the details of bulk updates in EF Core.

## Understanding the EF Core ChangeTracker

When you load entities from the database with EF Core, the `ChangeTracker` starts tracking them. As you update properties, delete entities, or add new ones, the `ChangeTracker` records these changes.

```csharp
using (var context = new AppDbContext())
{
    // Load a product
    var product = context.Products.FirstOrDefault(p => p.Id == 1);
    product.Price = 99.99; // Modify a property

    // At this point, the ChangeTracker knows that 'product' has been modified

    // Add a new product
    var newProduct = new Product { Name = "New Gadget", Price = 129.99 };
    context.Products.Add(newProduct);

    // Delete a product
    context.Products.Remove(product);

    context.SaveChanges(); // Persist all changes to the database
}
```

When you call `SaveChanges`, EF Core uses the `ChangeTracker` to determine which SQL commands to execute. This ensures that the database is perfectly synchronized with your modifications. The `ChangeTracker` acts as a bridge between your in-memory object model and your database.

If you're already familiar with how EF Core works, this serves mostly as a reminder.

## Bulk Updates and the ChangeTracker Disconnect

Now, let's focus on how [**bulk updates in EF Core**](https://www.milanjovanovic.tech/blog/how-to-use-the-new-bulk-update-feature-in-ef-core-7)interact with the `ChangeTracker` - or rather, how they don't interact with it. This design decision might seem counterintuitive, but there's a solid reason behind it: **performance**.

By directly executing SQL statements against the database, EF Core eliminates the overhead of tracking individual entity modifications.

```csharp
using (var context = new AppDbContext())
{
    // Increase price of all electronics by 10%
    context.Products
        .Where(p => p.Category == "Electronics")
        .ExecuteUpdate(
            s => s.SetProperty(p => p.Price, p => p.Price * 1.10));

    // In-memory Product instances with Category == "Electronics"
    // will STILL have their old price
}
```

In this example, we're increasing the price of all products in the `Electronics` category by 10%. The `ExecuteUpdate` method efficiently translates the operation into a single SQL `UPDATE` statement.

```csharp
UPDATE [p]
SET [p].[Price] = [p].[Price] * 1.10
FROM [Products] as [p];
```

However, if you inspect the `Product` instances that EF Core has already loaded into memory, you'll find that their `Price` properties haven't changed. This might seem surprising if you aren't aware of how bulk updates interact with the change tracker.

Everything we discussed up to this point also applies to the `ExecuteDelete` method.

[**EF Core interceptors**](https://www.milanjovanovic.tech/blog/how-to-use-ef-core-interceptors) do not trigger for `ExecuteUpdate` and `ExecuteDelete` operations. If you need to track or modify bulk update operations, you can create database triggers that fire whenever a relevant table is updated or deleted. This allows you to log details and perform additional actions.

## The Problem: Maintaining Consistency

If `ExecuteUpdate` completes successfully, the changes are directly committed to the database. This is because bulk operations bypass the `ChangeTracker` and don't participate in the usual transaction managed by `SaveChanges`.

If `SaveChanges` subsequently fails due to an error (e.g., validation error, database constraint violation, connection issue), you'll be in an inconsistent state. The changes made by `ExecuteUpdate` are already persisted. Any changes made "in memory" are lost.

The most reliable way to ensure consistency is to wrap both `ExecuteUpdate` and the operations that lead to `SaveChanges` in a transaction:

```csharp
using (var context = new AppDbContext())
using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        context.Products
            .Where(p => p.Category == "Electronics")
            .ExecuteUpdate(
                s => s.SetProperty(p => p.Price, p => p.Price * 1.10));

        // ... other operations that modify entities

        context.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        // You could also let the transaction go out of scope.
        // This would automatically rollback any changes.
        transaction.Rollback();

        // Proceed to handle the exception...
    }
}
```

If `SaveChanges` fails, the transaction will be rolled back, reverting the changes made by both `ExecuteUpdate` and any other operations within the transaction. This keeps your database in a consistent state.

## Summary

EF Core bulk update features, `ExecuteUpdate` and `ExecuteDelete`, are invaluable tools for optimizing performance. By bypassing the `ChangeTracker` and executing raw SQL directly, they deliver significant speed improvements compared to traditional methods.

However, it's crucial to be mindful of the potential pitfalls associated with this approach. The disconnect between in-memory entities and the database state can lead to unexpected results if not handled correctly.

My rule of thumb is to create an explicit [**database transaction**](https://www.milanjovanovic.tech/blog/working-with-transactions-in-ef-core) when I want to make additional entity changes. We can be confident that all the changes will persist in the database or none of them will.

I hope this was helpful, and I'll see you next week.

**P.S.** Get the [source code](https://github.com/m-jovanovic/ef-bulk-updates) and try out the examples from this issue.

* * *

**P.S. Whenever you're ready, there are 3 ways I can help you:**

1. [**Modular Monolith Architecture (NEW):**](https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) Join 650+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.

2. [**Pragmatic Clean Architecture:**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) Join 2,800+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.

3. [**Patreon Community:**](https://www.patreon.com/milanjovanovic) Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.