EF Core Concurrency

Concurrency conflicts occur when one user retrieves an entity's data to modify it, and then another user updates the same entity's data before the first user's changes are written to the database. How you handle those conflicts depends on the nature of the changes being made.

Last In Wins

In many cases, there is only one version of the truth, so it doesn't matter if one user's changes overwrite another's changes. In theory, the changes should result in the same update being made to the record. For example, it doesn't matter if two users attempt to update a sports fixture record with the final score. There is no need for any concurrency management strategy in this scenario. This is known as the last in-wins approach to concurrency control.

Pessimistic Concurrency

Pessimistic concurrency involves locking database records to prevent other users from being able to access/change them until the lock is released, much like when two users attempt to open the same file on a network share. However, the ability to lock records is not supported by all databases and can be complex to the program as well as highly resource intensive. It is simply not practical at all in disconnected scenarios such as web applications. Entity Framework Core provides no support for pessimistic concurrency control.

Optimistic Concurrency

Optimistic concurrency assumes that the update being made will be accepted, but before the change is made in the database, the original values of the record are compared to the existing row in the database and if any changes are detected, a concurrency exception is raised. This is useful in situations where allowing one user's changes to overwrite another's could lead to data loss. This could happen for example, if two users are looking at a customer record, and one of the users adds a missing telephone number. The second user alters the address, but the record that they alter was retrieved before the telephone number was added by the first user. When the second user commits their change (which won't include the telephone number), the first change will be lost. Entity Framework Core provides support for optimistic concurrency management.

Detecting Concurrency Conflicts

Entity Framework Core supports two approaches to concurrency conflict detection: configuring existing properties as concurrency tokens; and adding a "rowversion" property to act as a concurrency token.

Configuring existing properties

Properties can be configured as concurrency tokens via data annotations by applying the ConcurrencyCheck attribute:

language-csharp
|
public class Author
{
    public int AuthorId { get; set; }
    public string FirstName { get; set; }
    [ConcurrencyCheck]
    public string LastName { get; set; }
    public ICollection<Book> Books { get; set; }
}

Alternatively, properties can be configured using the Fluent API IsConcurrencyToken method:

language-csharp
|
public class SampleContext : DbContext
{
    public DbSet<Author> Authors { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Author>()
            .Property(a => a.LastName).IsConcurrencyToken();
    } 
}

public class Author
{
    public int AuthorId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public ICollection<Book> Books { get; set; }
}

Any existing properties that have been configured as concurrency tokens will be included with their original values in the WHERE clause of an UPDATE or DELETE statement. When the SQL command is executed, EF Core expects to find one row that matches the original values. If any of the configured columns have had their values changed between the time that the data was retrieved and the time that the changes are sent to the database, EF Core will throw a DbUpdateConcurrencyException with the message:

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

The concurrency token configuration can be applied to as many non-primary key properties as needed. Care needs to be taken as this approach can lead to very long WHERE clauses, or a lot of data being passed into them especially if any of the properties being configured as concurrency tokens are unlimited string values as is illustrated where, where the Biography field has been included as a concurrency token:

Entity Framework Core Concurrency Management

Adding a RowVersion property

The second approach to concurrency management involves adding a column to the database table to store a version stamp for the row of data. Different database systems approach this requirement in different ways. SQL Server offers the rowversion data type for this purpose. The column stores an incrementing number. Each time the data is inserted or modified, the number increments.

User A might retrieve a row of data, followed by User B. The rowversion value for the row will be the same for both users. If User B submits changes, the rowversion value in the table will increment by 1 for that row. If User A subsequently tries to modify the same record, the rowversion value in their WHERE clause combined with the primary key value will no longer match an existing row in the database and EF Core will throw a DbUpdateConcurrencyException.

A property must be a byte array data type to be mapped to a rowversion column. It can be configured to take part in concurrency checking by adding the TimeStamp data annotations attribute:

language-csharp
|
public class Author
{
    public int AuthorId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public ICollection<Book> Books { get; set; }
    [TimeStamp]
    public byte[] RowVersion { get; set; }
}

If you prefer to use the Fluent API to configure the property, you will use the IsRowVersion method:

language-csharp
|
public class SampleContext : DbContext
{
    public DbSet<Author> Authors { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Author>()
            .Property(a => a.RowVersion).IsRowVersion();
    } 
}

public class Author
{
    public int AuthorId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public ICollection<Book> Books { get; set; }
    public byte[] RowVersion { get; set; }
}

Note that the IsRowVersion method was added in EF Core 1.1. It is a convenience method that simplifies the previous approach which was to combine the IsConcurrencyToken method with the ValueGeneratedOnAddOrUpdate method:

language-csharp
|
public class SampleContext : DbContext
{
    public DbSet<Author> Authors { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Author>()
            .Property(a => a.RowVersion)
            .IsConcurrencyToken()
            .ValueGeneratedOnAddOrUpdate();
    } 
}

Either approach results in the same thing - the RowVersion column will be configured as a database type that provides automatic row-versioning (e.g. rowversion in SQL Server), rather than a varbinary type, which is the default mapping for byte array types.

The SaveChanges method should be called within a try-catch block so that any DbUpdateException exceptions can be caught and the appropriate action taken, such as presenting the newly updated record to the user:

language-csharp
|
try
{
    db.SaveChanges();
    // move on
}
catch (DbUpdateException)
{
    // get the latest version of the record for display
}

In a disconnected scenario such as a web application, you will most likely store the values of any concurrency tokens in hidden fields if they are not included as regular form fields. This is to ensure that they are available as parameter values to the WHERE clause of any UPDATE or DELETE statement.

FAQ

What is a DbUpdateConcurrencyException exception in EF Core?

In EF Core, a DbUpdateConcurrencyException is thrown when there's a conflict while trying to save changes to the database.

For example, let's say the current customer name is Joathan, but you want to fix the name and update it to Jonathan; meanwhile, already someone else has updated it to Mikael. A DbUpdateConcurrencyException must be manually solved to know if we should keep the new database value or overwrite it.


Why a DbUpdateConcurrencyException exception occurs in EF Core?

In EF Core, a DbUpdateConcurrencyException occurs to ensure that multiple users don't overwrite each other's data due to the optimistic concurrency strategy.

If there is a difference between the data you want to update and the current database value, EF Core raises the DbUpdateConcurrencyException to signal the concurrency conflict. This mechanism helps maintain data integrity in scenarios with concurrent data access.


When a DbUpdateConcurrencyException exception occurs in EF Core?

In EF Core, a DbUpdateConcurrencyException occurs when an attempt to save changes in the database is in conflict with a change made by another user or process before.

The DbUpdateConcurrencyException exception happens when the data is updated or deleted and has already been modified by another transaction after it was originally fetched, resulting in a concurrency conflict.


How is determined when to throw a DbUpdateConcurrencyException in EF Core?

In EF Core, the decision to throw a DbUpdateConcurrencyException is based on the number of affected rows returned. The check for the concurrency token is appended to the WHERE clause, and when performing an UPDATE or DELETE, if no rows were affected, it means there is currently a concurrency conflict.

Here is a more detailed answer:

  1. Concurrency Tokens: When defining your entity model, you can mark a property as a concurrency token:
  2. Database Operations: When performing an update or delete operation, EF Core includes these concurrency tokens in the SQL WHERE clause.
  3. Check the Affected Rows: After executing the operation, EF Core examines the number of affected rows.
    • If no rows were affected, it means that no records in the database matched. This is interpreted as a concurrency conflict.
  4. Throw the Exception: If such a conflict is detected, EF Core throws a DbUpdateConcurrencyException.

How to manually throw a DbUpdateConcurrencyException exception in EF Core?

You first need to create a new instance of the Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException class by specifying your error message.

throw new Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException("[Error Message]", new List<IUpdateEntry> { null }  );

See all DbUpdateConcurrencyException Constructors



Date Modified: 2023-08-22
Author:

Edit this page in GitHub

Got any EF Core Question?