Entity Framework Core: SaveChanges and Change Tracking

When working with Entity Framework Core (EF Core), you typically retrieve entities from the database, modify their properties, and then persist those changes back to the database. The primary mechanism for this is the DbContext.SaveChanges() method. This method, along with EF Core's built-in change tracking, handles the process of detecting and applying your modifications.

Understanding Change Tracking

EF Core maintains a shadow state for each entity it's aware of. This state includes information about:

This change tracking is crucial for SaveChanges() to know exactly what to do.

The SaveChanges() Method

When you call DbContext.SaveChanges(), EF Core performs the following steps:

  1. Detects Changes: EF Core iterates through all tracked entities and compares their current values with their original values (if they exist and the entity is not Added).
  2. Generates SQL Commands: Based on the detected changes, EF Core generates the necessary SQL `INSERT`, `UPDATE`, or `DELETE` statements to synchronize the database with the current state of your entities.
  3. Executes SQL Commands: The generated SQL commands are executed against the database.
  4. Updates Entity State: After the commands are executed, EF Core updates the tracking information for the affected entities, typically marking them as 'Unchanged'.

Common Scenarios and Usage

Adding New Entities

To add a new entity, create an instance of your entity class, set its properties, and add it to the appropriate DbSet in your DbContext.


using (var context = new YourDbContext())
{
    var newProduct = new Product
    {
        Name = "New Gadget",
        Price = 19.99M
    };
    context.Products.Add(newProduct);
    await context.SaveChangesAsync();
}
            

Modifying Existing Entities

When you retrieve an entity from the database, EF Core automatically tracks it. Any changes you make to its properties are automatically detected.


using (var context = new YourDbContext())
{
    var productToUpdate = await context.Products.FindAsync(1);
    if (productToUpdate != null)
    {
        productToUpdate.Price = 25.50M; // EF Core detects this change
        await context.SaveChangesAsync();
    }
}
            
Tip: You can also explicitly mark an entity as modified if needed, although this is rarely necessary for simple property updates.

context.Entry(productToUpdate).State = EntityState.Modified;
                

Deleting Entities

To delete an entity, you first need to retrieve it or attach it to the context, then mark it for deletion.


using (var context = new YourDbContext())
{
    var productToDelete = await context.Products.FindAsync(2);
    if (productToDelete != null)
    {
        context.Products.Remove(productToDelete); // Or context.Remove(productToDelete);
        await context.SaveChangesAsync();
    }
}
            

Saving Multiple Changes

SaveChanges() can handle multiple additions, modifications, and deletions in a single call. EF Core optimizes this by attempting to batch operations where possible.


using (var context = new YourDbContext())
{
    var newProduct1 = new Product { Name = "Widget A", Price = 10.00M };
    var newProduct2 = new Product { Name = "Gadget B", Price = 20.00M };
    context.Products.AddRange(newProduct1, newProduct2);

    var productToUpdate = await context.Products.FindAsync(3);
    if (productToUpdate != null)
    {
        productToUpdate.Price = 15.75M;
    }

    var productToDelete = await context.Products.FindAsync(4);
    if (productToDelete != null)
    {
        context.Remove(productToDelete);
    }

    await context.SaveChangesAsync();
}
            

SaveChanges(bool acceptAllChangesOnSuccess)

The SaveChanges() method has an overload that accepts a boolean parameter: SaveChanges(bool acceptAllChangesOnSuccess).

Consider this scenario:


// Suppose some entities are modified
await context.SaveChangesAsync(false); // Save changes, but keep them as Modified/Added in tracking

// Now you might want to re-fetch or perform other actions based on the *original* state
// before they were committed, or perhaps re-apply some logic.
// Be cautious: If you don't handle acceptAllChangesOnSuccess = false correctly,
// you might end up with unexpected behavior on subsequent SaveChanges calls.
            

Concurrency Handling

When multiple users or processes can modify the same data, concurrency conflicts can arise. EF Core supports various concurrency control strategies, most commonly optimistic concurrency.

With optimistic concurrency, you typically add a row version or timestamp property to your entity. When SaveChanges() executes an UPDATE statement, it includes a WHERE clause that checks if the row version property still matches the original value. If it doesn't, it means another process has modified the data since it was read, and EF Core will throw a DbUpdateConcurrencyException.

Important: You must handle DbUpdateConcurrencyException to gracefully manage conflicts, perhaps by retrying the operation, informing the user, or merging changes.

try
{
    await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    // Handle the concurrency conflict, e.g., refresh data, merge changes, etc.
    // For demonstration, we'll re-throw it here, but in a real app, you'd implement logic.
    throw;
}
            

Performance Considerations

Customizing SaveChanges

You can override the SaveChangesAsync() method in your derived DbContext class to inject custom logic before or after saving changes. This is often used for auditing or setting common properties like last modified dates.


public class YourDbContext : DbContext
{
    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // Logic before saving, e.g., audit trail
        var count = await base.SaveChangesAsync(cancellationToken);
        // Logic after saving, e.g., event notification
        return count;
    }
}