Most of the work I have done with SQLite has been on single-user systems, but recently I had to work on a SQLite project that was going to have a handful of concurrentĀ users and a subset of the user activities would need to deal with concurrency issues. In the past, in a situation like this, I have been using SQL Server and use the rowversionĀ or timestamp column typeĀ which places a unique value on the row on any updates or inserts.
There is a page in the official docs on Concurrency Tokens, but for me, it wasn’t super helpful. Thankfully after some searching, I came across the GitHub issueĀ In ASP.Net Core 2.x with Entity Framework Core, Concurrency Control not working with SQLiteĀ which had a solid sample as one of the replies. This post is going to walk through an example implementation of that sample. The starting point of the code can be found in this GitHub repo.
Sample Background
The sample project being used is a simple web application to manage a contact list. The repo contains an implementationĀ using Postgres and one using Sqlite. This whole post will only be touch files found in the Sqlite folder/project.
Model Changes and Data Migration
SQLite doesn’t have the concept of a timestamp column, but this solution is going to emulate one. To do this we are going to change theĀ Contact model found in theĀ Models folder. We are going to add aĀ Timestamp property with aĀ Timestamp data annotation. The following is the full model class with the new property at the bottom.
public class Contact { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } public string City { get; set; } public string Subregion { get; set; } public string PostalCode { get; set; } public string Phone { get; set; } public string Email { get; set; } [Timestamp] public byte[] Timestamp { get; set; } }
Next, let’s create a new migration with the change to the model. I’m using the .NET CLI so from a command prompt in the project directory run the following command.
dotnet ef migrations add ContactTimestamp --context ContactsDbContext
In theĀ MigrationsĀ directory, open newly created migration. It should be named something likeĀ *_ContactTimestamp.cs.Ā In theĀ Up function, we are going to add a couple of triggers to the new Timestamp column. These triggers are going to assign a random blob to the timestamp column when a row is inserted or updated which is how we are simulating the function of SQL Server’s Timestamp data type. The following is the fullĀ Up function with the added triggers.
protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<byte[]>( name: "Timestamp", table: "Contacts", rowVersion: true, nullable: true); migrationBuilder.Sql( @" CREATE TRIGGER SetContactTimestampOnUpdate AFTER UPDATE ON Contacts BEGIN UPDATE Contacts SET Timestamp = randomblob(8) WHERE rowid = NEW.rowid; END "); migrationBuilder.Sql( @" CREATE TRIGGER SetContactTimestampOnInsert AFTER INSERT ON Contacts BEGIN UPDATE Contacts SET Timestamp = randomblob(8) WHERE rowid = NEW.rowid; END "); }
To apply the migration to the database you can use the following command.
dotnet ef database update --context ContactsDbContext
Testing it out
Now for a quick and dirty test, we are going to add a ConcurrencyTest function to the existingĀ ContactsController. This function is going to ensure a specific contact exists, then pull the contact from two different DBContexts, make a mutation on the resulting contact objects, then attempt to save. The first save will work and the second should fail. Please note that this function isn’t an example of how things should be done just a quick and dirty way to prove that the concurrency check is happening.
[Route("ConcurrencyTest")] public void ConcurrencyTest() { var context1 = new ContactsDbContext(new DbContextOptionsBuilder<ContactsDbContext>() .UseSqlite("Data Source=Database.db").Options); var context2 = new ContactsDbContext(new DbContextOptionsBuilder<ContactsDbContext>() .UseSqlite("Data Source=Database.db").Options); var contactFromContext1 = context1.Contacts.FirstOrDefault(c => c.Name == "Test"); if (contactFromContext1 == null) { contactFromContext1 = new Contact { Name = "Test" }; context1.Add(contactFromContext1); context1.SaveChanges(); } var contactFromContext2 = context2.Contacts.FirstOrDefault(c => c.Name == "Test"); contactFromContext1.Address = DateTime.Now.ToString(); contactFromContext2.Address = DateTime.UtcNow.ToString(); context1.SaveChanges(); context2.SaveChanges(); }
Run the application and hit theĀ ConcurrenctTest route which isĀ http://localhost:1842/ConcurrencyTest for my test. The following is the resulting exception.
An unhandled exception occurred while processing the request.
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. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
Wrapping Up
While the information wasn’t the easiest in the world to locate, as you can see Entity Framework Core using SQLite has good support for concurrency control. The above is just one option for its implementation. I hope this saves you all so time.
The code in its final state can be found here.
Also published on Medium.
I am a little confused here. This is default functionality as documented here https://docs.microsoft.com/en-us/ef/core/modeling/concurrency#timestamprow-version
You list that exact URL and you say ā but for me, it wasnāt super helpful. ā in your code sample, literally all you did was add ā [Timestamp] public byte[] Timestamp { get; set; }ā this is literally what the docs say to do.
Even the github link you posted says everything works as expected. Something I did find truly confusing however is having to modify OriginalValues in order to trigger the concurrency detection. Scroll down for the code sample and look at the first line in ātryā https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application#detecting-concurrency-conflicts
The part that was helpful was that SQLite doesn’t have a timestamp field so it does not just work.