SQLite in ein .NET-Projekt integrieren
SQLite für .NET-Entwickler, Teil 2
Im ersten Teil dieser Serie haben wir gesehen, warum SQLite für überraschend viele Szenarien die bessere Datenbankwahl ist. Jetzt wird es konkret: Wie integriert man SQLite in ein .NET-Projekt? Die gute Nachricht: Das Ökosystem ist ausgereift. Microsoft selbst pflegt mit Microsoft.Data.Sqlite einen erstklassigen ADO.NET-Provider, Entity Framework Core unterstützt SQLite als vollwertiges Target, und für alle, die SQL lieber von Hand schreiben, ist Dapper nur ein NuGet-Paket entfernt.
Der Einstieg: Microsoft.Data.Sqlite
Microsofts eigener SQLite-Provider ist bewusst schlank gehalten. Anders als System.Data.SQLite, welches das gesamte ADO.NET-API implementiert, konzentriert sich Microsoft.Data.Sqlite auf das Wesentliche und verzichtet auf Features wie Connection Pooling; bei einer Embedded-Datenbank wäre das ohnehin kontraproduktiv. Die Verbindung ist ein einfacher Dateipfad, und die erste Abfrage lässt sich in wenigen Zeilen schreiben:
using var connection = new SqliteConnection( "Data Source=app.db"); connection.Open(); var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA journal_mode=WAL;"; cmd.ExecuteNonQuery();
Ein entscheidender Unterschied zu Server-Datenbanken: Bei SQLite sollte die Verbindung möglichst lange offen bleiben. Während man bei SQL Server oder PostgreSQL Connections kurz öffnet und schnell wieder freigibt, profitiert SQLite von einer dauerhaften Verbindung, idealerweise eine pro Anwendung als Singleton. Der Grund ist einfach: Es gibt keinen Server, zu dem eine Netzwerkverbindung aufgebaut wird. Die Connection ist nur ein Handle auf die Datei, und das Öffnen und Schließen erzeugt unnötigen Overhead durch wiederholtes Laden der Schema-Informationen.
In der Praxis bedeutet das: Man registriert die SqliteConnection als Singleton im DI-Container und setzt die PRAGMAs einmalig nach dem Öffnen. Wer im ersten Teil dieser Serie die fünf wichtigsten PRAGMA-Einstellungen gesehen hat, kann sie hier direkt übernehmen. Der Connection String unterstützt zusätzlich Parameter wie Mode=ReadWriteCreate und Cache=Shared, die das Verhalten der Datenbank weiter steuern.
Entity Framework Core: SQLite als First-Class Target
Für die meisten .NET-Projekte ist Entity Framework Core der Standard-ORM, und die SQLite-Integration gehört zu den am besten gepflegten Providern. Ein dotnet add package Microsoft.EntityFrameworkCore.Sqlite genügt, und der DbContext lässt sich wie gewohnt konfigurieren:
services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlite("Data Source=app.db"));Migrations funktionieren wie bei SQL Server, mit einer Einschränkung: SQLite unterstützt kein ALTER COLUMN und kein DROP COLUMN vor Version 3.35. EF Core löst das über Table Rebuilds, bei denen die Tabelle unter der Haube neu erstellt und die Daten kopiert werden. Das funktioniert zuverlässig, kann aber bei großen Tabellen dauern. Wer häufig Schema-Änderungen macht, sollte das im Auge behalten und gegebenenfalls auf eine neuere SQLite-Version setzen.
Der größte Vorteil von EF Core mit SQLite zeigt sich beim Testen. Statt einen Testcontainer mit SQL Server hochzufahren oder eine In-Memory-Datenbank zu verwenden, die sich subtil anders verhält, kann man einfach eine SQLite-Datei im Temp-Verzeichnis anlegen. Die Tests laufen gegen eine echte relationale Datenbank, aber ohne externen Server. Nach dem Testlauf löscht man die Datei oder behält sie zur Analyse. Einfacher geht Integrations-Testing nicht.
Dapper: SQL mit voller Kontrolle
Nicht jedes Projekt braucht einen ORM. Wer lieber SQL schreibt und die volle Kontrolle über die eigenen Queries behalten will, findet in Dapper den idealen Begleiter für SQLite. Dapper arbeitet als dünne Schicht über ADO.NET, mappt Ergebnisse direkt auf POCOs und erzeugt praktisch keinen Overhead. In Kombination mit SQLite ergibt das einen Stack, der in seiner Einfachheit kaum zu überbieten ist: Eine Datei als Datenbank, ein Micro-ORM für das Mapping, und SQL als einzige Abfragesprache.
Der Vorteil gegenüber EF Core liegt in der Transparenz. Man sieht jede Query, die an die Datenbank geht, und kann sie gezielt auf SQLite-spezifische Features optimieren. Wer etwa die JSON-Funktionen aus dem ersten Teil dieser Serie nutzen will, schreibt die Queries direkt in SQL und mappt das Ergebnis auf seine Klassen. Kein LINQ-to-SQL-Übersetzer steht dazwischen, der SQLite-spezifische Funktionen möglicherweise nicht kennt.
In der Praxis hat sich Dapper besonders für Szenarien bewährt, in denen Performance kritisch ist und die Queries überschaubar bleiben. Ein typisches Beispiel ist ein Read-Model für eine CQRS-Architektur, bei dem die Lesezugriffe über Dapper auf SQLite laufen und die Schreibseite über EF Core oder direkt über ADO.NET abgewickelt wird. Die Trennung fühlt sich natürlich an, weil SQLite ohnehin zwischen Lese- und Schreibpfad differenziert.
PRAGMA-Konfiguration für die Produktion
Im ersten Teil dieser Serie haben wir die fünf wichtigsten PRAGMAs vorgestellt. In einem .NET-Projekt stellt sich die Frage, wo und wann diese gesetzt werden. Die Antwort hängt vom gewählten Datenzugriff ab. Bei Microsoft.Data.Sqlite setzt man die PRAGMAs direkt nach dem Öffnen der Verbindung. Bei EF Core gibt es einen eleganteren Weg über die DbContext-Konfiguration:
protected override void OnConfiguring(
DbContextOptionsBuilder options)
{
var conn = new SqliteConnection(
"Data Source=app.db");
conn.Open();
conn.Execute("PRAGMA journal_mode=WAL");
conn.Execute("PRAGMA synchronous=NORMAL");
options.UseSqlite(conn);
}Der Trick liegt darin, die Connection vorab zu öffnen, die PRAGMAs zu setzen und dann die bereits geöffnete Connection an EF Core zu übergeben. So stellt man sicher, dass WAL-Modus und Cache-Einstellungen aktiv sind, bevor die erste Migration läuft. Alternativ kann man die PRAGMAs auch über einen DbCommandInterceptor setzen, der bei jeder neuen Connection automatisch feuert – besonders praktisch, wenn man den DbContext als scoped registriert.
Besonders wichtig für den Produktionsbetrieb ist das Zusammenspiel von Connection-Management und Concurrency. In einer ASP.NET-Core-Anwendung kommen Requests parallel an, und jeder Request braucht potenziell Zugriff auf die Datenbank. Die bewährte Strategie: eine Singleton-Connection für Leseoperationen und eine zweite, synchronisierte Connection für Schreibzugriffe. So nutzt man den WAL-Modus optimal aus, ohne in SQLITE_BUSY-Fehler zu laufen.
Vom Prototyp zur Produktion
Der eleganteste Aspekt von SQLite in .NET ist die Migration vom Prototyp zur Produktion. Ein Projekt, das mit SQLite als schneller Testdatenbank startet, kann ohne Code-Änderung in Produktion gehen, vorausgesetzt, die Anforderungen passen zum Profil. Kein Datenbankserver muss provisioniert werden, kein Connection String muss auf einen externen Host zeigen, kein Terraform-Modul muss geschrieben werden. Die Datenbank liegt als Datei im Anwendungsverzeichnis, und das Deployment ist ein simples File-Copy.
Für das Monitoring in der Produktion bietet sich ein einfacher Health-Check an, der die Datenbankdatei auf Integrität prüft. SQLite liefert dafür das PRAGMA integrity_check, das in einem ASP.NET-Core-Health-Check-Endpoint aufgerufen werden kann. Zusammen mit der Dateigröße und der WAL-Checkpoint-Statistik hat man die wichtigsten Metriken für den operativen Betrieb im Blick – ohne externes Monitoring-Tool.
Für Szenarien, in denen man später doch auf eine Server-Datenbank wechseln möchte, zahlt sich EF Core als Abstraktionsschicht aus. Der Wechsel von UseSqlite zu UseNpgsql oder UseSqlServer ist eine Zeile Code, plus eventuell angepasste Migrations. Die Geschäftslogik bleibt unberührt. Wer allerdings bewusst SQLite-spezifische Features wie JSON-Funktionen oder Generated Columns nutzt, bindet sich stärker an die Plattform. Das ist kein Nachteil, solange die Entscheidung bewusst getroffen wird.
Im nächsten Teil dieser Serie gehen wir einen Schritt weiter und schauen uns an, was passiert, wenn SQLite über die Grenzen einer einzelnen Instanz hinauswachsen soll. Mit libSQL und Turso gibt es inzwischen ein Ökosystem, das SQLite um Replikation, Geo-Distribution und sogar Vektorsuche erweitert, ohne die Einfachheit aufzugeben.