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:
- Change Tracking: EF Core tracks all entities that are loaded into the
DbContext
or are explicitly added, modified, or removed. - 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). - SQL Generation: Based on the detected changes, EF Core generates the necessary SQL statements.
- Database Execution: These SQL statements are then executed against the database.
- 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()
:
- You must use the
await
keyword to get the result. - The containing method must be marked with the
async
keyword.
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:
- Retrying the operation.
- Merging changes.
- Discarding the user's changes and reloading the latest data.
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();
}
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
- Batching: EF Core efficiently batches multiple operations into a single database roundtrip, significantly improving performance.
SaveChanges(true)
: CallingSaveChanges(true)
(orSaveChangesAsync(true)
) forces EF Core to re-detect changes for all tracked entities, which can be slower if only a few entities have changed. It's generally better to let EF Core automatically detect changes.Load=
When loading entities for modification, consider using query options that prevent loading related data if it's not needed for the update operation.
:False
SaveChanges()
.