-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Hi,
I'm experiencing a data loss issue when using EF Core with sqlite in a multi threaded scenario. This bug seems to only happen if PRAGMA journal_mode=DELETE
. I can't replicate it in WAL
mode.
What's happening is that sometimes when a new object is added and SaveChanges
is called on a context, the database insert appears to run successfully, and DatabaseGenerated
primary keys are created by the DB and set against the C# object, but when the database is later queried for that primary key the record doesn't exist. What's more, the next time an object is created and saved to the database it gets assigned the same primary key as the previous object. It's almost like it rolled-back the first transaction without throwing an exception.
This issue does not exist in EF Core 6, it started in EF Core 7. We have found that adding
ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>()
to our options builder resolves this issue, so it seems to be a bug in the new SqliteUpdateSqlGenerator
.
Sample Program
This program sets up a database with PRAGMA journal_mode=DELETE
, creates two dBContexts for one sqlite database and then adds objects to the contexts in one-off tasks which run in parallel. After each addition it checks to ensure the primary keys set in each context's call to SaveChanges
is unique. If the keys are not unique it throws an exception.
What I expect to happen:
The program runs to completion with no exceptions.
What actually happens:
The program throws an exception because the objects added to the two different contexts ended up with the same primary key.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
</ItemGroup>
</Project>
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;
using Microsoft.EntityFrameworkCore.Update;
namespace collisionTest;
public class LogEvent
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public required string Message { get; set; }
}
public class LogDbContext : DbContext
{
public DbSet<LogEvent> LogEvents => Set<LogEvent>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data source=testDb.db");
// Bug can be worked around by using legacy update SQL generator
// .ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();
}
}
public class Program
{
static async Task Main(string[] _)
{
using (var contextToCreateDb = new LogDbContext())
{
contextToCreateDb.Database.EnsureDeleted();
contextToCreateDb.Database.EnsureCreated();
using var connection = contextToCreateDb.Database.GetDbConnection();
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "PRAGMA journal_mode=DELETE;";
command.ExecuteNonQuery();
}
using var contextOne = new LogDbContext();
using var contextTwo = new LogDbContext();
for (int i = 0; i < 500; i++)
{
var taskOne = Task.Run(() =>
{
var logEvent = new LogEvent(){ Message = "Log Message" };
contextOne.Add(logEvent);
contextOne.SaveChanges();
return logEvent.Id;
});
var taskTwo = Task.Run(() =>
{
var logEvent = new LogEvent(){ Message = "Log Message" };
contextTwo.Add(logEvent);
contextTwo.SaveChanges();
return logEvent.Id;
});
await taskOne;
await taskTwo;
if (taskOne.Result == taskTwo.Result)
{
throw new Exception($"Duplicate IDs Returned: {taskOne.Result}");
}
}
}
}
Include provider and version information
EF Core version: 7.0.5
Database provider: Microsoft.EntityFrameworkCore.Sqlite
Target framework: .NET 7.0
Operating system: Ubuntu 18.04 & Ubuntu 20.04
IDE: VS Code