Concurrency Management in Entity Framework Core

Concurrency Conflicts

Concurrency conflicts occur when one user retrieves an entity's data in order 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 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 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 prior to the change being 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 an additional "rowversion" property to act as a concurrency token.

Configuring existing properties

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, 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.

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

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:

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; }
}

The configuration can be applied to as many non-primary key properties as needed. However, while this approach keeps the entity classes free of non-entity related properties, it 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:

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 a valid 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:

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 IsConcurrencyToken method as with the previous approach, but you will also specify a value generation strategy via the ValueGeneratedOnAddOrUpdate method for the column to be created as a rowversion type instead of a varbinary type:

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();
    } 
}

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; }
}

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:

try
{
    db.SaveChanges();
    // move on
}
catch (DbUpdateException)
{
    // get latest version of 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.


Created:
Last updated: 29/03/2017 07:13:02
Proficiency Level: Intermediate