Optimistic concurrency: IsConcurrencyToken and RowVersion

c# concurrency ef-code-first entity-framework sql-server

Question

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();
1
13
8/9/2016 5:36:41 PM

Accepted Answer

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; }
22
11/25/2017 10:38:03 AM

Popular Answer

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)



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow