Introduction to Saving Changes
Entity Framework Core (EF Core) provides a powerful and flexible mechanism for persisting changes made to your entity objects back to the database. This process involves tracking changes to entities and then executing the necessary SQL commands to synchronize the database with your application's state.
The core of saving changes in EF Core revolves around the DbContext
. When you modify entities that are tracked by a DbContext
instance, EF Core keeps track of these changes. You then call the SaveChanges()
method on the DbContext
to send these changes to the database.
Tracking Changes
EF Core automatically tracks changes to entities that are loaded into the DbContext
. When an entity is retrieved from the database via the DbContext
, EF Core stores its original state. Any subsequent modifications to properties of this entity are then compared against this original state.
The following entity states can be tracked:
- Added: The entity is new and will be inserted into the database.
- Unchanged: The entity has not been modified since it was last retrieved or saved.
- Modified: The entity's properties have been changed and will be updated in the database.
- Deleted: The entity will be deleted from the database.
The SaveChanges()
Method
The SaveChanges()
method is the primary way to persist changes. When you call it, EF Core:
- Detects all pending changes across all tracked entities.
- Generates the appropriate SQL commands (
INSERT
,UPDATE
,DELETE
). - Executes these commands against the database in a single transaction by default.
- Updates the entities in the
DbContext
with the latest database state (e.g., retrieving generated IDs).
SaveChanges()
operates within a database transaction. If any of the operations within the batch fail, the entire transaction is rolled back, ensuring data consistency.
Example: Adding, Modifying, and Deleting Entities
Let's consider a simple scenario with a Blog
entity.
First, ensure you have your DbContext
and entity class set up:
<?csharp
// Entity class
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public string Title { get; set; }
}
// DbContext class
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=BloggingEFCore;Trusted_Connection=True;");
}
}
?>
Now, let's see how to add, modify, and delete blogs:
<?csharp
using (var context = new BloggingContext())
{
// 1. Adding a new blog
var newBlog = new Blog { Url = "http://example.com/new", Title = "My New Blog" };
context.Blogs.Add(newBlog);
Console.WriteLine("Added new blog to context.");
// 2. Modifying an existing blog
var existingBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://example.com/existing");
if (existingBlog != null)
{
existingBlog.Title = "Updated Title for Existing Blog";
Console.WriteLine($"Marked existing blog for update: {existingBlog.BlogId}");
}
// 3. Deleting a blog
var blogToDelete = context.Blogs.Find(3); // Assuming blog with ID 3 exists
if (blogToDelete != null)
{
context.Blogs.Remove(blogToDelete);
Console.WriteLine($"Marked blog for deletion: {blogToDelete.BlogId}");
}
// 4. Saving all changes to the database
int changesSaved = context.SaveChanges();
Console.WriteLine($"{changesSaved} changes saved to the database.");
}
?>
SaveChanges(bool acceptAllChangesOnSuccess)
The SaveChanges()
method has an overload that accepts a boolean parameter acceptAllChangesOnSuccess
. By default, this is set to true
, meaning that after a successful save, EF Core resets the change tracking for all entities that were part of the save operation. If you set it to false
, EF Core will retain the current state of entities in the change tracker, allowing you to potentially perform further operations on them without them being marked as unchanged immediately.
Detecting and Handling Concurrency Conflicts
Concurrency conflicts can occur when multiple users or processes try to modify the same data simultaneously. EF Core can help detect these conflicts. When a conflict is detected during SaveChanges()
, an DbUpdateConcurrencyException
is thrown.
You can handle this exception by:
- Reloading the original values from the database.
- Ignoring the concurrency conflict.
- Attempting to merge the changes.
<?csharp
try
{
int rowsAffected = context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// Handle concurrency conflicts here
Console.WriteLine("Concurrency conflict detected.");
// Option 1: Reload original values and re-apply current values
var entry = ex.Entries.Single();
var databaseValues = entry.GetDatabaseValues();
if (databaseValues == null)
{
// The entity was deleted by another user.
Console.WriteLine("The entity has been deleted by another user.");
}
else
{
// Restore the original values and mark the entity as modified.
entry.OriginalValues.SetValues(databaseValues);
// Re-apply your current values to the entity
// For example, if you had a property 'Name':
// entry.Entity.Name = yourCurrentName;
Console.WriteLine("Original values reloaded. Please re-apply your changes and try saving again.");
}
// Alternatively, you might choose to just refresh and not save the current changes.
// context.Entry(entry.Entity).Reload();
}
?>
SaveChangesAsync()
For asynchronous operations, EF Core provides SaveChangesAsync()
. This method is preferred in modern .NET applications, especially in web or UI scenarios, to avoid blocking threads.
<?csharp
// In an async method
public async Task SaveBlogChangesAsync(Blog blog)
{
using (var context = new BloggingContext())
{
context.Blogs.Update(blog); // Or Add/Remove as needed
await context.SaveChangesAsync();
Console.WriteLine("Changes saved asynchronously.");
}
}
?>
When using SaveChangesAsync()
, ensure your calling method is marked with async
and uses the await
keyword.
SaveChanges(ICollection<TEntity> entities)
EF Core 7 introduced the ability to save changes for a specific collection of entities without affecting other entities tracked by the DbContext
. This is done using the SaveChanges(ICollection<TEntity> entities)
overload.
<?csharp
using System.Collections.Generic;
using System.Linq;
// ... inside a method with access to context and blogs
var blogsToSaveChanges = context.Blogs.Where(b => b.Url.Contains("example.com")).ToList();
if (blogsToSaveChanges.Any())
{
int changes = context.SaveChanges(blogsToSaveChanges);
Console.WriteLine($"Saved changes for {changes} specific blogs.");
}
?>
This overload is particularly useful for optimizing performance when you only need to persist changes for a subset of entities.