Serverunabhängige Datenbanken mit C#, SQLite und dem Entity Framework
Ausgangssituation
Vor kurzem habe ich für einen Kunden eine Anwendung geschrieben, welche in mehreren Schritten zehntausende Dateien aus einem SharePoint verarbeitet. Zusätzlich musste das ganze noch modular anwendbar sein. Sprich, das Sammeln der Metadaten und der spätere Verarbeitungsprozess mussten unabhängig von einander funktionieren.
Doch wo sollten die gesammelten Daten gespeichert werden?
Im Memory? Was wenn die Anwendung abstürzt oder neu gestartet werden muss? Irgendwo auf der Festplatte in einer CSV?
Zudem war die tatsächliche Anzahl von zu verarbeiteten Dateien zum Entwicklungszeitpunkt noch nicht bekannt und die Applikation musste auf einem SharePoint-Server laufen, auf dem Memory und Speicherplatz bekanntlich sowieso Mangelware sind.
Die eigentlich recht offensichtliche Lösung: Eine Datenbank.
Doch nun hatte ich leider auch keinen Zugriff auf einen Datenbank-Server, was also tun?
Die Antwort: SQLite – aber was ist das eigentlich?
SQLite ist eine relationale, serverunabhängige Datenbank-Engine, welche Weltweit überall dort Einsatz findet, wo Daten lokal gespeichert und verarbeitet werden müssen. Beispielsweise auf Smartphones, in Browsern, Betriebssystemen und vielem mehr.
Zusätzlich habe ich mich noch dazu entschieden auch das Entity Framework (Code-First Approach) einzubauen, um die SQL-Abfragen zu abstrahieren und zu erleichtern.
Aber nun zum interessanten Teil!
Aufbau der Anwendung
Zuerst wird eine C# Konsolen-Anwendung erstellt:
Im Anschluss müssen die benötigten Nuget-Pakete für das EF und SQLite installiert werden:
Die EF-Installation hat in der app.config schon einige Einträge hinzugefügt. Bisher fehlt aber noch der Connection-String, über welchen die Verbindung zur Datenbank aufgebaut wird. In meinem Fall sieht dieser wie folgt aus:
<connectionStrings>
<add name= "TodoDatabase" providerName="System.Data.SQLite.EF6" connectionString="Data
Source=C:\temp\sqlite\tododatabase.db" />
</connectionStrings>
Jetzt fehlen noch zwei essentielle Dinge: 1. Das Entity und 2. der Database Context.
Für dieses Blog-Beispiel habe ich mich für ein simples Todo-Entity entschieden:
Die Klasse besteht aus einer Id
(der Primary-Key, welcher später aus einer Guid zusammengesetzt wird), einer Description
und einem Status-Enum
Gelöscht:
public class Todo
{
public string Id { get ; set ; }
public string Description { get ; set ; }
public StatusType Status { get ; set ; }
}
Nun muss der DbContext angelegt werden. Dieser stellt die Brücke zwischen den Entitäten und der Datenbank dar. Über ihn gelingt das Lesen und Schreiben, sowie das Erstellen der Relationen zwischen Entitäten. Der DbContext kann noch einiges mehr, in diesem Beispiel beschränke ich mich aber auf das Minimum.
So sollte der Context aussehen:
public class DatabaseContext : DbContext
{
public DatabaseContext() : base ( "name=TodoDatabase" )
{
Database.ExecuteSqlCommand(
"CREATE TABLE IF NOT EXISTS 'Todo' ('Id' TEXT PRIMARY KEY, 'Description' TEXT, 'Status' INTEGER)" );
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
base .OnModelCreating(modelBuilder);
}
public DbSet<Todo> Todos { get ; set ; }
}
Jeder der schonmal einen DatabaseContext erstellt hat, wird direkt über zwei Dinge stolpern, aber fangen wir vorne an:
Unsere Klasse erbt von DbContext, das ist notwendig um überhaupt die Funktionalität eines DatabaseContext’s zu gewährleisten.
Dem DbContext-Konstruktor übergeben wir den Namen des vorhin definierten Connection-Strings.
Und schon taucht die erste Eigenart von SQLite auf: Der Driver supported leider keine Migrations
und somit auch keine automatisierte Erstellung der Datenbank.
Es gibt aber einige Workarounds für dieses Problem.
Ich habe mich für den Ansatz entschieden, doch ein wenig SQL zu schreiben und die Tabelle über die Methode ExcecuteSqlCommand
anzulegen. Falls du den selben Ansatz benutzen möchtet, denk an das IF NOT EXISTS
.
In der Methode OnModelCreating
muss noch das EF-Feature PluralizingTableNameConvention
entfernt werden, welches ansonsten dazu führt, dass die aus den Entitäten entstehenden Tabellen im Plural angelegt werden:
Aus der Entität Todo
würde also die Tabelle Todos
werden. In Verbindung mit den Fehlenden Migrations, führt auch das zu Problemen.
Und zum Schluss wird über ein DbSet der Zugriff auf die Todo-Tabelle gewährleistet, damit diese später mit LINQ
-Queries manipuliert werden kann.
Jetzt, wo die ganze Konfiguration endlich abgeschlossen ist, kann mit dem befüllen der Datenbank gestartet werden.
class Program
{
static void Main( string [] args)
{
using (var context = new DatabaseContext())
{
context.Todos.AddRange( new List<Todo>
{
new Todo
{
Id = new Guid().ToString(),
Description = "Mein offenes Todo" ,
Status = StatusType.Todo
},
new Todo
{
Id = new Guid().ToString(),
Description = "Mein laufendes Todo" ,
Status = StatusType.InProgress
},
new Todo
{
Id = new Guid().ToString(),
Description = "Mein abgeschlossenes Todo" ,
Status = StatusType.Done
}
});
context.SaveChanges();
}
}
}
Hier wird in der Datenbank ein Todo für jeden Status erstellt. Dazu wird der DatabaseContext (Achtung: disposable) geöffnet und über das vorhin erstellte DbSet auf unsere Tabelle zugegriffen.
Und auch die Abfragen über Linq-Queries funktionieren:
class Program
{
static void Main( string [] args)
{
using (var context = new DatabaseContext())
{
var todosInProgress = context.Todos.Where(todo => todo.Status == StatusType.InProgress).ToList();
foreach (var todoInProgress in todosInProgress)
{
Console.WriteLine($ "Todo: {todoInProgress.Description}" );
}
}
}
}
Falls beim Ausführen der Fehler “No Entity Framework provider found for the ADO.NET provider with invariant name ‘System.Data.SQLite'” auftreten sollte, muss die app.config wie folgt angepasst werden:
aktuelle Konfiguration:
<entityFramework>
<providers>
<provider invariantName= "System.Data.SqlClient"
type="System.Data.Entity.SqlServer.SQLiteProviderServices,
EntityFramework.SqlServer" />
<provider invariantName= "System.Data.SQLite.EF6"
type= "System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
</providers>
</entityFramework>
neue Konfiguration:
<entityFramework>
<providers>
<provider invariantName="System.Data.SQLite"
type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
</providers>
</entityFramework>
Zusätzlich sollte darauf geachtet werden, dass der im Connection-String angegebene Pfad auch tatsächlich existiert. Dieser wird nicht automatisch erstellt und führt ansonsten auch zu einem sehr irreführendem Fehler.
Fazit
SQLite stellt in bestimmten Szenarien eine gute alternative zu klassischen Datenbank-Servern dar, bietet eine sehr hohe Performance und lässt sich, abgesehen von ein paar kleineren Stolpersteinen bei der Konfiguration, sehr gut mit dem Entity-Framework verbinden. Ein großer Nachteil sind die fehlenden Migrations, für welche zwar Workarounds existieren, man allerdings nicht an das Feature-Set von MSSQL-Datenbanken herankommt. Bei Weiterentwicklung der Tabellen kommt ihr also nicht drum herum, eure Datenbank zu löschen und neu generieren zu lassen. Falls die Möglichkeit besteht dotnet-Core zu verwenden, gibt es aber auch bei diesem Punkt weniger Probleme.
Auch wenn sicher dieser Blog auf Konsolen-Anwendungen bezieht, funktioniert die Umsetzung natürlich auch mit WebAPI- oder WPF- oder sonstigen Apps.
Als Management-Tool für die Datenbank empfiehlt sich der SQLite Browser, welcher hier heruntergeladen werden kann.
Happy Coding!