I try to use optimistic concurrency check in EF Core with SQLite. The simplest positive scenario (even without concurrency itself) gives me Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: 'Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded
.
Entity:
public class Blog { public Guid Id { get; set; } public string Name { get; set; } public byte[] Timestamp { get; set; } }
Context:
internal class Context : DbContext { public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite(@"Data Source=D:\incoming\test.db"); ///optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasKey(p => p.Id); modelBuilder.Entity<Blog>() .Property(p => p.Timestamp) .IsRowVersion() .HasDefaultValueSql("CURRENT_TIMESTAMP"); } }
Sample:
internal class Program { public static void Main(string[] args) { var id = Guid.NewGuid(); using (var db = new Context()) { db.Database.EnsureDeleted(); db.Database.EnsureCreated(); db.Blogs.Add(new Blog { Id = id, Name = "1" }); db.SaveChanges(); } using (var db = new Context()) { var existing = db.Blogs.Find(id); existing.Name = "2"; db.SaveChanges(); // Exception thrown: 'Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException' } } }
I suspect it's something to do with the data types between EF and SQLite. Logging gives me the following query on my update:
Executing DbCommand [Parameters=[@p1='2bcc42f5-5fd9-4cd6-b0a0-d1b843022a4b' (DbType = String), @p0='2' (Size = 1), @p2='0x323031382D31302D30372030393A34393A3331' (Size = 19) (DbType = String)], CommandType='Text', CommandTimeout='30'] UPDATE "Blogs" SET "Name" = @p0 WHERE "Id" = @p1 AND "Timestamp" = @p2;
But the column types are BLOB for both Id and Timestamp (SQLite does not provide UUID and timestamp column types):
At the same time if I use SQL Server (use commented connection string + remove .HasDefaultValueSql("CURRENT_TIMESTAMP")
), sample works correctly and updates timestamp in the DB.
Used packages:
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />
Have I configured the model for concurrency check wrong? That drives me crazy that I can't make it work with this simplest scenario.
UPDATE: how I finally made it work. Here only idea is shown, but probably it helps anybody:
public class Blog { public Guid Id { get; set; } public string Name { get; set; } public long Version { get; set; } } internal class Context : DbContext { public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite(@"Data Source=D:\incoming\test.db"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasKey(p => p.Id); modelBuilder.Entity<Blog>() .Property(p => p.Version) .IsConcurrencyToken(); } } internal class Program { public static void Main(string[] args) { var id = Guid.NewGuid(); long ver; using (var db = new Context()) { db.Database.EnsureDeleted(); db.Database.EnsureCreated(); var res = db.Blogs.Add(new Blog { Id = id, Name = "xxx", Version = DateTime.Now.Ticks}); db.SaveChanges(); } using (var db = new Context()) { var existing = db.Blogs.Find(id); existing.Name = "yyy"; existing.Version = DateTime.Now.Ticks; db.SaveChanges(); // success } using (var db = new Context()) { var existing = db.Blogs.Find(id); existing.Name = "zzz"; existing.Version = DateTime.Now.Ticks; db.SaveChanges(); // success } var t1 = Task.Run(() => { using (var db = new Context()) { var existing = db.Blogs.Find(id); existing.Name = "yyy"; existing.Version = DateTime.Now.Ticks; db.SaveChanges(); } }); var t2 = Task.Run(() => { using (var db = new Context()) { var existing = db.Blogs.Find(id); existing.Name = "zzz"; existing.Version = DateTime.Now.Ticks; db.SaveChanges(); } }); Task.WaitAll(t1, t2); // one of the tasks throws DbUpdateConcurrencyException } }