Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optimistic concurrency: IsConcurrencyToken and RowVersion

I'm creating the default concurrency strategy that I will use in my application.

I decided for an optimistic strategy.

All of my entities are mapped as Table per Type (TPT) (using inheritance). I soon learned that there is a problem when using columns of type RowVersion with inheritance on Entity Framework:

Product

Id INT IDENTITY PRIMARY KEY
RowVersion ROWVERSION

Car (inherits Product records)

Color TYNIINT NOT NULL,
AnotherProperty....   

If I update a record of the Car table the RowVersion column from Product table will not be updated.

I plan to use a column of type datetime2 (7) in Product and update it manually if any records of the tables that inherit this table are modified.

I think I'm reinventing the wheel.

Is there another way to use the optimistic concurrency strategy with ROWVERSION when using Table per Type (TPT) in Entity Framework?

Edit

My mapping:

class Product
{
    int Id { get; set; }
    string Name { get; set; }
    byte[] RowVersion { get; set; }
}

class Car : Product
{
    int Color { get; set; }
}

CodeFirst conventions.

Only the RowVersion property on Product entity has custom definitions:

modelBuilder.Entity<Product>() 
    .Property(t => t.RowVersion) 
    .IsConcurrencyToken();
like image 385
Vinicius Gonçalves Avatar asked Jul 09 '15 23:07

Vinicius Gonçalves


People also ask

What is IsConcurrencyToken?

The IsConcurrencyToken method is used to specify that a property should be included in a WHERE clause in an UPDATE or DELETE statement as part of concurrency management. public class SampleContext : DbContext. { public DbSet<Author> Authors { get; set; } Entity<Author>()

What is optimistic concurrency in Entity Framework?

EF Core implements optimistic concurrency control, meaning that it will let multiple processes or users make changes independently without the overhead of synchronization or locking. In the ideal situation, these changes will not interfere with each other and therefore will be able to succeed.

What are the two approaches for concurrency tokens that the EF provides state which approach is not recommended?

EF provides two approaches for concurrency tokens: Applying [ConcurrencyCheck] or IsConcurrencyToken to a property on the model. This approach is not recommended. For more information, see Concurrency Tokens in EF Core.

What is Rowversion in SQL Server?

rowversion is generally used as a mechanism for version-stamping table rows. The storage size is 8 bytes. The rowversion data type is just an incrementing number and does not preserve a date or a time. To record a date or time, use a datetime2 data type.


2 Answers

Both in EF6 and EF-core, when working with Sql Server, you have to use this mapping:

modelBuilder.Entity<Product>() 
.Property(t => t.RowVersion) 
.IsRowVersion(); // Not: IsConcurrencyToken

IsConcurrencyToken does configure a property as concurrency token, but (when using it for a byte[] property)

  • the data type is varbinary(max)
  • its value is always null if you don't initialize it
  • its value is not auto-incremented when a record is updated.

IsRowVersion on the other hand,

  • has datatype rowversion (in Sql Server, or timestamp in earlier versions), so
  • its value is never null, and
  • its value is always auto-incremented when a record is updated.
  • and it automatically configures the property to be an optimistic concurrency token.

Now when you update a Car you'll see two update statements:

DECLARE @p int
UPDATE [dbo].[Product]
SET @p = 0
WHERE (([Id] = @0) AND ([Rowversion] = @1))
SELECT [Rowversion]
FROM [dbo].[Product]
WHERE @@ROWCOUNT > 0 AND [Id] = @0

UPDATE [dbo].[Car]
SET ...

The first statement doesn't update anything, but it increments the rowversion, and it will throw a concurrency exception if the rowversion was changed in-between.

The [System.ComponentModel.DataAnnotations.Schema.Timestamp] attribute is the data annotations equivalent of IsRowVersion():

[Timestamp]
public byte[] RowVersion { get; set; }
like image 141
Gert Arnold Avatar answered Sep 17 '22 14:09

Gert Arnold


After a bit of investigating I was able to use IsConcurrencyToken on a byte[8] column called RowVersion in Entity Framework 6.

Because we want to use the same datatype in DB2 ( which doesn't have rowversion in the database itself) we can't use the option IsRowVersion()!

I investigated a little bit further how to work with IsConcurrencyToken.

I did the following to achieve a solution that seems to work:

My Model:

    public interface IConcurrencyEnabled
{
    byte[] RowVersion { get; set; }
}

  public class Product : AuditableEntity<Guid>,IProduct,IConcurrencyEnabled
{
    public string Name
    {
        get; set;
    }
    public string Description
    {
        get; set;
    }
    private byte[] _rowVersion = new byte[8];
    public byte[] RowVersion
    {
        get
        {
            return _rowVersion;
        }

        set
        {
            System.Array.Copy(value, _rowVersion, 8);
        }
    }
}

IConcurrencyEnabled is used to identify Entities that have a rowversion that needs special treatment.

I used fluent API to configure the modelbuilder:

    public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        Property(e => e.RowVersion).IsFixedLength().HasMaxLength(8).IsConcurrencyToken();
    }
}

And finally I added a method to my derived DBContext class to update the field before the base.SaveChanges is called:

        public void OnBeforeSaveChanges(DbContext dbContext)
    {
        foreach (var dbEntityEntry in dbContext.ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified))
        {
            IConcurrencyEnabled entity = dbEntityEntry.Entity as IConcurrencyEnabled;
            if (entity != null)
            {

                if (dbEntityEntry.State == EntityState.Added)
                {
                    var rowversion = dbEntityEntry.Property("RowVersion");
                    rowversion.CurrentValue = BitConverter.GetBytes((Int64)1);
                }
                else if (dbEntityEntry.State == EntityState.Modified)
                {
                    var valueBefore = new byte[8];
                    System.Array.Copy(dbEntityEntry.OriginalValues.GetValue<byte[]>("RowVersion"), valueBefore, 8);

                    var value = BitConverter.ToInt64(entity.RowVersion, 0);
                    if (value == Int64.MaxValue)
                        value = 1;
                    else value++;

                    var rowversion = dbEntityEntry.Property("RowVersion");
                    rowversion.CurrentValue = BitConverter.GetBytes((Int64)value);
                    rowversion.OriginalValue = valueBefore;//This is the magic line!!

                }

            }
        }
    }

The problem most people encounter is that after setting the value of the entity, we always get a UpdateDBConcurrencyException, because the OriginalValue has changed... even if it hasn't!

The reason is that for a byte[] both original and currentValue change if you set the CurrentValue alone (?? strange and unexpected behavior).

So I set the OriginalValue again to the original Value before I updated the rowversion... Also I copy the array to avoid referencing the same byte-array!

Attention: Here I use an incremental approach to change the rowversion, you are free to use your own strategy to fill in this value. (Random or time-based)

like image 35
Schwarzie2478 Avatar answered Sep 19 '22 14:09

Schwarzie2478