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:
- The original values of entity properties.
- The current values of entity properties.
- The current state of the entity (e.g., Added, Modified, Deleted, Unchanged).
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:
- 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).
- 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.
- Executes SQL Commands: The generated SQL commands are executed against the database.
- 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();
}
}
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)
.
-
If
acceptAllChangesOnSuccess
istrue
(the default), EF Core will reset the tracking information for all successfully saved entities to 'Unchanged'. This is the most common and generally desired behavior. -
If
acceptAllChangesOnSuccess
isfalse
, EF Core will not reset the tracking information for entities that were successfully saved. This can be useful in scenarios where you want to retry saving or perform further operations on the entities immediately after a save operation, without losing the context of their modified state.
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
.
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
-
Batching:
SaveChanges()
can perform multiple operations efficiently. Avoid callingSaveChanges()
in a loop for individual entities. -
Large Number of Changes: For very large numbers of changes, consider using
Microsoft.EntityFrameworkCore.BulkExtensions
for significantly faster bulk inserts, updates, and deletes, though these bypass some EF Core tracking mechanisms. -
Transaction Management:
SaveChanges()
is transactional by default. If you need to combine multipleDbContext
operations into a single larger transaction, you can useDbContext.Database.BeginTransactionAsync()
.
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;
}
}