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();
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)
varbinary(max)
null
if you don't initialize itIsRowVersion on the other hand,
rowversion
(in Sql Server, or timestamp
in earlier versions), soNow 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; }
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)