Turso and Ahtola Join RepoDB's SQLite Family
We are excited to announce two new SQLite variants for RepoDB: RepoDb.Sqlite.Turso and RepoDb.Sqlite.Ahtola. They join RepoDb.Sqlite.Microsoft in our SQLite family. You get the same fluent CRUD, batch and raw-SQL operations, on a choice of three SQLite-compatible engines.
Both providers were contributed by Marc-André Moreau (@mamoreau-devolutions). Thank you, Marc-André!
Why this matters
For years, using SQLite in .NET has meant one engine: the original C library, through Microsoft.Data.Sqlite. It is proven and fast, and it remains the right default for most projects. But new SQLite-compatible engines now go where the original can’t:
- Turso is a rewrite of SQLite in Rust. It keeps SQLite’s SQL dialect and file format, and adds what the original lacks: concurrent writes through MVCC (
BEGIN CONCURRENT), asynchronous I/O, change data capture, and vector support. Its driver also documents hosted remote databases and embedded replicas. - Ahtola is a fully managed C# port of Turso’s engine, from Devolutions. It has no native library, no P/Invoke and no Rust toolchain, and its core ADO.NET path is trim- and NativeAOT-clean.
Which problems they solve
- Write contention. Classic SQLite allows a single writer at a time. If your app is held back by
database is lockederrors, Turso’s concurrent writes are designed for that. - Native dependencies. The original SQLite ships a native binary for each platform, which can be awkward for trimmed, NativeAOT, sandboxed or unusual targets. Ahtola is pure .NET, so there is nothing native to ship.
- Local-first and edge apps. Turso’s remote and embedded-replica support makes SQLite an option for apps that keep a local copy of data that also lives in the cloud.
They are not the faster drivers. In the benchmarks run for these providers, both were slower than Microsoft.Data.Sqlite for RepoDB’s workloads, but they solve the problems Sqlite have not. Choose them for what they enable, not for speed.
Why RepoDB alongside them
Both engines are young. Turso has not reached 1.0, and Ahtola describes itself as experimental and not yet production-ready. Adopting a young engine is a reasonable bet, but it shouldn’t take over your whole codebase. RepoDB keeps it contained:
- One API, three engines.
RepoDb.Sqlite.Microsoft,RepoDb.Sqlite.TursoandRepoDb.Sqlite.Ahtolashare the same operations. To switch engines, you change a package reference, ausingline and the bootstrap call. Your queries, entities and repositories stay the same. - Less risk. Start on the proven engine and try Turso or Ahtola on one service. If an engine doesn’t work out, moving back is a small change, not a rewrite.
- The driver differences are already handled. Turso resolves some column types only after the first row is read, and Ahtola returns
NUMERIC/DECIMALvalues as strings. RepoDB handles both, so your entities don’t have to. - Room to grow. If your app outgrows SQLite, the same RepoDB API covers SQL Server, PostgreSQL, MySQL and more than a dozen other databases.
Getting started
Pick the engine you want. For Turso:
> Install-Package RepoDb.Sqlite.Turso -Version 0.0.1-alpha1
For Ahtola:
> Install-Package RepoDb.Sqlite.Ahtola -Version 0.0.1-alpha1
Initialize it once at startup. Turso works best with automatic type conversion turned on:
// Turso (using Turso.Data.Sqlite;)
GlobalConfiguration
.Setup(new() { ConversionType = ConversionType.Automatic })
.UseTurso();
// Ahtola (using Ahtola.Data.Sqlite;)
GlobalConfiguration
.Setup()
.UseAhtola();
Both drivers expose a SqliteConnection class, so the samples below work for either engine. Only the using line at the top of your file changes. For a complete walkthrough, follow the Get Started with SQLite tutorial.
CRUD
Insert
using var connection = new SqliteConnection("Data Source=app.db");
var person = new Person { Name = "John Doe", Age = 30, CreatedDateUtc = DateTime.UtcNow };
var id = connection.Insert(person);
Query
using var connection = new SqliteConnection("Data Source=app.db");
var person = connection.Query<Person>(e => e.Id == 10045).FirstOrDefault();
Update
using var connection = new SqliteConnection("Data Source=app.db");
var person = connection.Query<Person>(e => e.Id == 10045).FirstOrDefault();
person.Age = 31;
var updatedRows = connection.Update(person);
Merge
using var connection = new SqliteConnection("Data Source=app.db");
var person = new Person { Id = 10045, Name = "John Doe", Age = 32, CreatedDateUtc = DateTime.UtcNow };
var id = connection.Merge(person);
Delete
using var connection = new SqliteConnection("Data Source=app.db");
var deletedRows = connection.Delete<Person>(10045);
Batch Operations
The InsertAll, UpdateAll, MergeAll and DeleteAll operations send multiple rows per round-trip. The number of rows per round-trip is controlled by batchSize.
InsertAll
The generated identities are set back on the entities.
using var connection = new SqliteConnection("Data Source=app.db");
var people = GetPeople(1000);
var insertedRows = connection.InsertAll(people, batchSize: 100);
UpdateAll
using var connection = new SqliteConnection("Data Source=app.db");
var people = connection.Query<Person>(e => e.Age < 18).AsList();
people.ForEach(p => p.Age += 1);
var updatedRows = connection.UpdateAll(people, batchSize: 100);
MergeAll
using var connection = new SqliteConnection("Data Source=app.db");
var people = GetPeople(1000);
var mergedRows = connection.MergeAll(people, batchSize: 100);
DeleteAll
using var connection = new SqliteConnection("Data Source=app.db");
var people = connection.Query<Person>(e => e.Age > 100);
var deletedRows = connection.DeleteAll(people);
Raw-SQL
ExecuteQuery
using var connection = new SqliteConnection("Data Source=app.db");
var people = connection.ExecuteQuery<Person>("SELECT * FROM [Person] WHERE [Age] >= @Age;",
new { Age = 18 });
ExecuteScalar
using var connection = new SqliteConnection("Data Source=app.db");
var count = connection.ExecuteScalar<long>("SELECT COUNT(*) FROM [Person];");
A preview, not a final release
Both packages are previews at v0.0.1-alpha1, and their APIs and behavior may change before a stable release. A few things are worth knowing up front:
- .NET 8.0 or later. Neither driver supports .NET Framework or .NET Standard.
- Use one SQLite provider per process. Loading more than one of RepoDB’s SQLite providers in the same process has a known
InsertAllcaching issue. - Turso:
Truncatedeletes the rows but does not runVACUUM, so file space is not reclaimed andAUTOINCREMENTis not reset. The integration tests ran against the local engine; remote and embedded-replica connections have not yet been tested against a hosted Turso endpoint. - Ahtola: the engine itself is experimental. Its authors recommend Turso’s official bindings for production workloads.
- Renamed packages. If you tried the earlier
RepoDb.TursoorRepoDb.Ahtolabuilds, switch toRepoDb.Sqlite.TursoandRepoDb.Sqlite.Ahtola. The namespaces changed too.
The limitations page has the details.
We would love to hear your feedback, especially if you’re running either engine somewhere Microsoft.Data.Sqlite didn’t fit. It directly shapes what we prioritize on the way to general availability.
~ Conceptualized by me. Reviewed and checked by AI. Refined and gatekept by me. ~
Please support us
Give a ⭐ to our Github repository |
Follow me at X / Twitter |
Connect with us at our official Teams Channel
Apache License 2.0 — Copyright © 2026 by Michael Camara Pendon / Create a Request