Saving Data Changes with Entity Framework Core
This document guides you through the process of saving changes made to entities within your Entity Framework Core (EF Core) data model back to the database. EF Core provides a robust and flexible mechanism for handling these operations efficiently.
Understanding the Change Tracker
EF Core's core functionality for managing data changes is its Change Tracker. When you query entities from the database, the Change Tracker is aware of their original state. As you modify properties, add new entities, or remove existing ones, the Change Tracker records these modifications.
The SaveChanges() Method
The primary method for persisting changes to the database is DbContext.SaveChanges()
. When this method is called, EF Core examines the Change Tracker for all detected changes and generates the necessary SQL commands (INSERT, UPDATE, DELETE) to synchronize the database with your entity states.
Basic Usage
Here's a typical workflow for saving changes:
- Instantiate your
DbContext
. - Query one or more entities from the database.
- Modify properties of existing entities or add/remove entities.
- Call
await _context.SaveChangesAsync()
to persist the changes.
using (var context = new MyDbContext())
{
// Query an existing blog
var blog = await context.Blogs.FindAsync(1);
if (blog != null)
{
blog.Url = "https://new-url.com";
// Mark for update (implicitly handled by EF Core on SaveChanges)
}
// Add a new post
var newPost = new Post { Title = "New Article", Content = "Lorem ipsum...", BlogId = 1 };
context.Posts.Add(newPost);
// Remove a post (example, if needed)
var postToRemove = await context.Posts.FindAsync(2);
if (postToRemove != null)
{
context.Posts.Remove(postToRemove);
}
// Save all changes
int affectedRows = await context.SaveChangesAsync();
Console.WriteLine($"{affectedRows} rows affected.");
}
Managing Entity States
While EF Core automatically tracks many state changes, you can also explicitly manage entity states using the EntityEntry
API:
EntityState.Added
: The entity is new and will be inserted.EntityState.Modified
: The entity's properties have been changed and it will be updated.EntityState.Deleted
: The entity will be deleted.EntityState.Unchanged
: The entity has not been modified.EntityState.Detached
: The entity is not tracked by the context.
var blog = new Blog { Url = "https://example.com" };
context.Entry(blog).State = EntityState.Added;
await context.SaveChangesAsync();
Concurrency Control
Concurrency conflicts can occur when multiple users or processes attempt to modify the same data simultaneously. EF Core supports several concurrency control strategies:
- Optimistic Concurrency: This is the default and most common approach. It involves adding a row versioning column (e.g., a timestamp or an integer) to your entities. When an entity is updated, the version column is also updated. EF Core checks this version during
SaveChanges()
. If the database version differs from the one in the context, aDbUpdateConcurrencyException
is thrown. - Pessimistic Concurrency: This approach locks the database row when it's read, preventing others from modifying it until the transaction is complete. This is typically implemented using database-specific locking mechanisms.
[Timestamp]
attribute (for byte arrays) or by using manual configuration in OnModelCreating
.
Handling DbUpdateConcurrencyException
When a concurrency conflict occurs during SaveChanges()
, you'll receive a DbUpdateConcurrencyException
. You can handle this by:
- Refreshing the entity from the database and re-applying your changes.
- Discarding your changes and using the database values.
- Merging your changes with the database values.
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// Refresh entities from database
foreach (var entry in ex.Entries)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null)
{
// Entity was deleted by another user
Console.WriteLine($"Entity {entry.Metadata.Name} with key {entry.GetUnsafeHashValue()} was deleted.");
// Handle deletion scenario, e.g., remove from context
entry.State = EntityState.Detached;
}
else
{
// Reload original values from database
var databaseEntity = (YourEntityType)databaseValues.ToObject();
// Restore values that were not changed by the current operation
var originalValues = context.Entry(entry.Entity).OriginalValues;
foreach (var property in originalValues.Properties)
{
if (!entry.OriginalValues.IsModified(property.Name))
{
originalValues[property.Name] = databaseEntity.GetType().GetProperty(property.Name).GetValue(databaseEntity);
}
}
// Update the original values to reflect the database
entry.OriginalValues.SetValues(databaseValues);
// Re-throw or prompt user to resolve conflict
// For simplicity, we'll just reload and let the user re-save.
Console.WriteLine($"Concurrency conflict detected for entity {entry.Entity.GetType().Name}. Reloading from database.");
entry.CurrentValues.SetValues(databaseValues); // Update current values to match database
entry.State = EntityState.Unchanged; // Mark as unchanged to avoid re-saving immediately
}
}
// Optionally, re-throw the exception or try SaveChanges again
// throw;
}
Bulk Operations
For scenarios involving a large number of insertions, updates, or deletions, consider EF Core's bulk operations capabilities. While not part of the core SaveChanges()
, libraries like EntityFrameworkCore.BulkExtensions can significantly improve performance by reducing round trips to the database.
Transaction Management
DbContext.SaveChangesAsync()
by default executes within a single database transaction. If you need to perform multiple SaveChanges()
operations as part of a larger logical unit of work, you can manually control transactions:
- Begin a transaction using
DbContext.Database.BeginTransactionAsync()
. - Perform your
SaveChanges()
calls. - Commit the transaction if all operations succeed using
transaction.Commit()
. - Rollback the transaction if any operation fails using
transaction.Rollback()
.
using (var transaction = await context.Database.BeginTransactionAsync())
{
try
{
// Perform first save
await context.SaveChangesAsync();
// Perform second save
await context.SaveChangesAsync(); // This will automatically use the existing transaction
// Commit if all operations were successful
await transaction.CommitAsync();
Console.WriteLine("Transaction committed.");
}
catch (Exception ex)
{
// Rollback on error
await transaction.RollbackAsync();
Console.WriteLine($"Transaction rolled back: {ex.Message}");
// Handle or re-throw the exception
}
}
Understanding and effectively utilizing these features will ensure efficient and reliable data persistence in your EF Core applications.