Concurrency Management in Entity Framework Core

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

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

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 concurrecny 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:

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:

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:

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:

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.

Last updated: 23/03/2018 08:13:59

© 2018 - Learn Entity Framework Core.
All rights reserved.