SaveChanges() and SaveChangesAsync()

The core mechanism for persisting changes made to your entities back to the database in Entity Framework Core is the SaveChanges() and its asynchronous counterpart, SaveChangesAsync(), method on the DbContext.

When you retrieve entities from the database, modify their properties, add new entities, or mark existing entities for deletion, EF Core's change tracker keeps a record of these modifications. The SaveChanges() method inspects these tracked changes and generates the appropriate SQL commands (INSERT, UPDATE, DELETE) to synchronize the database with the current state of your entities.

How it Works

The process generally involves these steps:

  1. Change Tracking: EF Core tracks all entities that are loaded into the DbContext or are explicitly added, modified, or removed.
  2. Change Detection: When SaveChanges() is called, EF Core traverses the tracked entities and compares their current state with their original state (or marks new entities as added, and deleted entities as deleted).
  3. SQL Generation: Based on the detected changes, EF Core generates the necessary SQL statements.
  4. Database Execution: These SQL statements are then executed against the database.
  5. State Update: After successful execution, EF Core updates the state of the entities in the context to reflect the changes made in the database, such as updating primary key values for newly inserted entities.

Basic Usage

Here's a simple example demonstrating how to add, modify, and delete entities and then save those changes:


using (var context = new BloggingContext())
{
    // Add a new blog
    var newBlog = new Blog { Url = "http://example.com/new" };
    context.Blogs.Add(newBlog);

    // Find an existing blog and modify it
    var existingBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://example.com");
    if (existingBlog != null)
    {
        existingBlog.Rating = 5;
        context.Entry(existingBlog).State = EntityState.Modified; // Explicitly mark as modified (often not needed for direct property changes)
    }

    // Mark a blog for deletion
    var blogToDelete = context.Blogs.FirstOrDefault(b => b.Url == "http://example.com/old");
    if (blogToDelete != null)
    {
        context.Blogs.Remove(blogToDelete);
    }

    // Save all changes to the database
    int rowsAffected = context.SaveChanges();
    Console.WriteLine($"{rowsAffected} rows affected.");
}
            

Asynchronous Operations: SaveChangesAsync()

For I/O-bound operations like database interactions, using asynchronous methods is crucial to prevent blocking the main thread, especially in web applications. SaveChangesAsync() performs the same function as SaveChanges() but returns a Task.


using (var context = new BloggingContext())
{
    // ... (add, modify, delete entities as before) ...

    // Save all changes asynchronously
    int rowsAffected = await context.SaveChangesAsync();
    Console.WriteLine($"{rowsAffected} rows affected.");
}
            

Key points for SaveChangesAsync():

Note: When you change a property on an entity that is already tracked by EF Core, the change tracker automatically detects the modification. You usually don't need to explicitly set the state to EntityState.Modified unless you're attaching an entity that EF Core isn't already tracking, or if you need to reset the state for some reason.

Concurrency Conflicts

Concurrency conflicts can occur when multiple users or processes attempt to modify the same data simultaneously. EF Core can detect and handle these conflicts. By default, if a conflict is detected during SaveChanges(), an DbUpdateConcurrencyException is thrown.

You can handle these exceptions by implementing specific strategies, such as:


try
{
    await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    // Handle the concurrency conflict
    // For example, reload the entities from the database and reapply local changes
    var entries = ex.Entries.ToList();
    foreach (var entry in entries)
    {
        var databaseEntry = context.Entry(entry.Entity);
        if (databaseEntry.State == EntityState.Modified)
        {
            // Reload original values from the database
            await databaseEntry.ReloadAsync();

            // Reapply local changes to the reloaded entity (or decide how to merge)
            var originalValues = databaseEntry.OriginalValues;
            var proposedValues = entry.CurrentValues;
            var databaseValues = databaseEntry.CurrentValues;

            // Example: Choose to keep the database values and notify the user
            foreach (var property in proposedValues.Properties)
            {
                var proposedValue = proposedValues[property.Name];
                var databaseValue = databaseValues[property.Name];
                if (!object.Equals(proposedValue, databaseValue))
                {
                    // Conflict detected in this property, decide what to do
                    // For simplicity, we'll just keep the database value here
                    // In a real app, you'd present a UI to the user
                    databaseEntry.CurrentValues[property.Name] = databaseValue; // Keep database value
                }
            }
            // Mark the entry as modified again if needed, or resolve manually
            // context.Entry(entry.Entity).CurrentValues.SetValues(databaseEntry.CurrentValues);
        }
    }
    // Retry saving the changes
    await context.SaveChangesAsync();
}
            
Warning: Implementing robust concurrency handling requires careful consideration of your application's requirements and user experience. The example above is a simplified illustration.

Transactions

By default, EF Core wraps calls to SaveChanges() in a database transaction. This ensures that if any of the generated SQL commands fail, the entire operation is rolled back, maintaining data integrity. You can also explicitly manage transactions for more complex scenarios.


using (var transaction = await context.Database.BeginTransactionAsync())
{
    try
    {
        // Perform multiple operations
        context.Blogs.Add(new Blog { Url = "http://example.com/tx1" });
        await context.SaveChangesAsync();

        context.Blogs.Add(new Blog { Url = "http://example.com/tx2" });
        await context.SaveChangesAsync();

        // If everything is successful, commit the transaction
        await transaction.CommitAsync();
        Console.WriteLine("Transaction committed successfully.");
    }
    catch (Exception ex)
    {
        // If any error occurs, roll back the transaction
        await transaction.RollbackAsync();
        Console.WriteLine($"Transaction rolled back: {ex.Message}");
        // Handle the exception appropriately
    }
}
            

Performance Considerations

Tip: For bulk operations, consider using EF Core's bulk update or insert features (often available through third-party libraries or specific EF Core versions/providers) which can be more performant than repeatedly calling SaveChanges().