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.

Photo by Kevin Ku on Unsplash

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.