Updating entity in EF Core application with SQLite gives DbUpdateConcurrencyException
Looks like EF Core SQLite provider does not handle properly [TimeStamp]
(or IsRowVersion()
) marked byte[]
properties when binding them to SQL query parameters. It uses the default byte[]
to hex string
conversion which is not applicable in this case - the byte[]
actually is a string
.
First consider reporting it to their issue tracker. Then, until it gets resolved (if ever), as a workaround you can use the following custom ValueConverter
:
class SqliteTimestampConverter : ValueConverter<byte[], string>
{
public SqliteTimestampConverter() : base(
v => v == null ? null : ToDb(v),
v => v == null ? null : FromDb(v))
{ }
static byte[] FromDb(string v) =>
v.Select(c => (byte)c).ToArray(); // Encoding.ASCII.GetString(v)
static string ToDb(byte[] v) =>
new string(v.Select(b => (char)b).ToArray()); // Encoding.ASCII.GetBytes(v))
}
Unfortunately there is no way to tell EF Core to use it only for parameters, so after assigning it with .HasConversion(new SqliteTimestampConverter())
, now the db type is considered string
, so you need to add .HasColumnType("BLOB")
.
The final working mapping is
modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion()
.HasConversion(new SqliteTimestampConverter())
.HasColumnType("BLOB")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
You can avoid all that by adding the following custom SQLite RowVersion "convention" at the end of your OnModelCreating
:
if (Database.IsSqlite())
{
var timestampProperties = modelBuilder.Model
.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(byte[])
&& p.ValueGenerated == ValueGenerated.OnAddOrUpdate
&& p.IsConcurrencyToken);
foreach (var property in timestampProperties)
{
property.SetValueConverter(new SqliteTimestampConverter());
property.Relational().DefaultValueSql = "CURRENT_TIMESTAMP";
}
}
so your property configuration could be trimmed down to
modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion();
or totally removed and replaced with data annotation
public class Blog
{
public Guid Id { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
Inspired by this thread on GitHub and the Ivan's answer I wrote this code to ensure on my unit testing to mimic the SQL Server concurrency.
var connection = new SqliteConnection("DataSource=:memory:");
var options = new DbContextOptionsBuilder<ActiveContext>()
.UseSqlite(connection)
.Options;
var ctx = new ActiveContext(options);
if (connection.State != System.Data.ConnectionState.Open)
{
connection.Open();
ctx.Database.EnsureCreated();
var tables = ctx.Model.GetEntityTypes();
foreach (var table in tables)
{
var props = table.GetProperties()
.Where(p => p.ClrType == typeof(byte[])
&& p.ValueGenerated == Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate
&& p.IsConcurrencyToken);
var tableName = table.Relational().TableName;
foreach (var field in props)
{
string[] SQLs = new string[] {
$@"CREATE TRIGGER Set{tableName}_{field.Name}OnUpdate
AFTER UPDATE ON {tableName}
BEGIN
UPDATE {tableName}
SET RowVersion = randomblob(8)
WHERE rowid = NEW.rowid;
END
",
$@"CREATE TRIGGER Set{tableName}_{field.Name}OnInsert
AFTER INSERT ON {tableName}
BEGIN
UPDATE {tableName}
SET RowVersion = randomblob(8)
WHERE rowid = NEW.rowid;
END
"
};
foreach (var sql in SQLs)
{
using (var command = connection.CreateCommand())
{
command.CommandText = sql;
command.ExecuteNonQuery();
}
}
}
}
}