Entity Framework Core

MSDN Documentation

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:

  1. Instantiate your DbContext.
  2. Query one or more entities from the database.
  3. Modify properties of existing entities or add/remove entities.
  4. 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, a DbUpdateConcurrencyException 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.
Tip: Configure optimistic concurrency by adding a property with the [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:

  1. Begin a transaction using DbContext.Database.BeginTransactionAsync().
  2. Perform your SaveChanges() calls.
  3. Commit the transaction if all operations succeed using transaction.Commit().
  4. 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.