Search code examples
c#sql-serverentity-frameworkconcurrencyef-code-first

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

Solution

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

    Note that the official documentation is not correct. It says that IsConcurrencyToken is the fluent equivalent of the [Timestamp] attribute. However, IsRowVersion is the equivalent.