Bulk on EFCore with ExecuteUpdate and ExecuteDelete

Pierre Belin
Pierre Belin
Bulk on EFCore with ExecuteUpdate and ExecuteDelete
Table of Contents
Table of Contents

In the evolving landscape of Entity Framework Core (EF Core), the processes of updating and deleting entities remain central to database operations. As EF Core progresses with each version, these operations are refined for efficiency and ease of use.

This article aims to shed light on two significant enhancements in the latest EF Core: the ExecuteUpdate and ExecuteDelete methods. Introduced to streamline batch update and delete operations, these methods represent a leap in how developers interact with data, emphasizing performance and simplicity.

Understanding ExecuteUpdate and ExecuteDelete in EFCore

ExecuteUpdate and ExecuteDelete were introduced in EF Core 6.0 as part of its continued commitment to improving data manipulation capabilities. These methods were designed to address the performance concerns associated with large-scale data operations.

Previously, developers had to rely on less efficient ways to update or delete multiple records, which often led to performance bottlenecks. With these new methods, EF Core provides a direct way to execute update and delete operations in the database, bypassing the need for retrieving and processing records in memory.

While both ExecuteUpdate and ExecuteDelete offer similar improvements in EF Core, their internal behavior and implementation are alike. For the sake of brevity and focus, this article will concentrate on ExecuteUpdate.

The understanding and concepts derived from this method are easily transferable to ExecuteDelete, providing a comprehensive view of their capabilities.

Consider the Goat entity, which has three properties: Id, Name, Age, and LastUpdateDateTime. In C#, the class definition would look like this:

public class Goat
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime LastUpdateDateTime { get; set; }
}

To demonstrate ExecuteUpdate in action, let's start by setting up a connection to a SQLite database and creating the Goat table. This involves defining a DbContext and configuring it for SQLite:

public class GoatDbContext : DbContext
{
    public DbSet<Goat> Goats { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=goats.db");
    }
}

The first example uses a traditional approach, iterating over a list of Goat instances and updating each one:

using (var context = new GoatDbContext())

var goats = context.Goats.ToList();
foreach (var goat in goats)
{
    goat.Name = "XXX";
    goat.LastUpdateDateTime = DateTime.UtcNow;
}
context.SaveChanges();

The second example leverages ExecuteUpdate:

context.Goats.ExecuteUpdate(b => b.SetProperty(g => g.Name, "XXX")
                                 .SetProperty(g => g.LastUpdateDateTime, DateTime.UtcNow));

In the first example, each Goat entity is loaded into memory, modified, and then saved back to the database. This process is resource-intensive, especially with a large dataset.

To give you an idea, during a mass modification, EFCore will produce the following query for each modified line. If you modify 1000 lines, 1000 SQL queries will be executed.

-- First update
UPDATE "Goats" SET "LastUpdateDateTime" = @p0, "Name" = @p1
WHERE "Id" = @p2
RETURNING 1;

-- Second update
UPDATE "Goats" SET "LastUpdateDateTime" = @p0, "Name" = @p1
WHERE "Id" = @p2
RETURNING 1;

-- And so on

In contrast, the ExecuteUpdate method directly translates the update operation into SQL, executing it at the database level without loading entities into memory. This results in significant performance gains, particularly noticeable in large-scale operations.

-- One query to update all lines
UPDATE "Goats" AS "g"
SET "LastUpdateDateTime" = rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', 'now'), '0'), '.'),
    "Name" = 'XXX'

Advanced Topics in Execute

Affected Rows

EF Core's ExecuteUpdate method returns an integer representing the number of rows affected. This can be useful for logging or validation purposes. Here's an example:

int affectedRows = context.Goats.ExecuteUpdate(b => b.SetProperty(g => g.Name, "NewName"));
Console.WriteLine($"{affectedRows} rows updated.");

Asynchronous Operations

EF Core also provides asynchronous versions of these methods, enhancing performance in scenarios that benefit from non-blocking database operations. Here's a combined example for both ExecuteUpdateAsync and ExecuteDeleteAsync:

await context.Goats.ExecuteUpdateAsync(b => b.SetProperty(g => g.Name, "AsyncUpdatedName"));
await context.Goats.ExecuteDeleteAsync(g => g.Name == "AsyncUpdatedName");

Tracking Management

One of the nuanced aspects of ExecuteUpdate in EF Core is its interaction with the ORM's tracking mechanism. Unlike traditional update methods where entities are tracked for changes, ExecuteUpdate operates outside this tracking scope.

To illustrate, let's consider an example where we first use ExecuteUpdate and then attempt to modify the same entities using the standard tracking mechanism:

var goat = context.Goats.FirstOrDefault();
// Here goat.Age == 10 

// ExecuteUpdate without tracking
context.Goats.ExecuteUpdate(b => b.SetProperty(g => g.Age, 15));

// goat.Name still 
goat.Age += 2;
context.SaveChanges(); // This won't affect entities previously updated by ExecuteUpdate
// goat.Age will be 12 and not 17 in database

In this scenario, the changes made by ExecuteUpdate are not tracked by EF Core's change tracker. Therefore, subsequent modifications to the same entities are not aware of the previous ExecuteUpdate operations. In the example, since the value is retrieved before the ExecuteUpdate is executed, the age is not associated with the Goat variable, since there is no tracking. Only the addition of the base value and the increment gives the final result, which is 12.

💡
As a result, it is usually a good idea to avoid mixing both tracked SaveChanges modifications and untracked modifications via ExecuteUpdate/ExecuteDelete.
-- Microsoft documentation

Atomicity in transaction

An important consideration when using ExecuteUpdate in EF Core is ensuring atomicity, especially in operations involving multiple data changes. Atomicity guarantees that a series of database operations either all succeed or all fail, maintaining data integrity.

The most reliable way to ensure atomicity with ExecuteUpdate is through the use of database transactions. Transactions encapsulate multiple operations into a single unit, which is either entirely committed or entirely rolled back. This ensures that partial updates do not occur, preserving the consistency of your database.

No ExecuteAdd

The absence of an ExecuteAdd method in EF Core is primarily due to the nature of insert operations. Unlike updates and deletes, inserts typically require individual handling of each entity for identity generation, relationships, and other constraints.

EF Core's design philosophy emphasizes the importance of context in insert operations, which doesn't lend itself well to a bulk operation approach akin to ExecuteUpdate or ExecuteDelete.

Limitations in TPC and TPT

ExecuteUpdate and ExecuteDelete are not applicable for entities that are not leaf nodes in Table-per-Concrete Type (TPC) and Table-per-Type (TPT) hierarchies in EF Core. This limitation arises from the complexity of generating SQL statements for entities in a hierarchy.

In TPC and TPT, each type in the hierarchy can have its own table structure, making it challenging to create a single SQL statement that can universally apply updates or deletes across the hierarchy.

Therefore, these methods are constrained to entities that represent leaf nodes in these inheritance models, where such SQL generation is straightforward and unambiguous.

Conclusion

The ExecuteUpdate method in EF Core represents a significant advancement in handling bulk data operations. By enabling direct database-level updates, it offers a more efficient and performant approach compared to traditional entity loading and modification methods. As EF Core continues to evolve, such enhancements play a crucial role in simplifying and optimizing data manipulation tasks for developers.

To go further:

ExecuteUpdate and ExecuteDelete - EF Core
Using ExecuteUpdate and ExecuteDelete to save changes with Entity Framework Core

Have a goat day 🐐



Join the conversation.

Great! Check your inbox and click the link
Great! Next, complete checkout for full access to Goat Review
Welcome back! You've successfully signed in
You've successfully subscribed to Goat Review
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated