Datenbanken & ORMs in .NET MAUI — Realm, Entity Framework, SQLite und Dapper im Vergleich
Ein praktischer Vergleich von vier beliebten Ansätzen zur Datenspeicherung in .NET MAUI-Anwendungen. Leistung, Entwicklererfahrung und Empfehlungen für verschiedene Anwendungsfälle.
- .NET MAUI
- SQLite
- Entity Framework
- Realm
- Dapper
Einführung
Jede mobile Anwendung muss Daten lokal speichern — sei es Benutzereinstellungen, zwischengespeicherte API-Antworten oder vollständige Offline-First-Datensätze. Im .NET MAUI-Ökosystem stehen uns mehrere ausgereifte Optionen zur Verfügung, jede mit eigenen Vor- und Nachteilen.
Nach Jahren der Entwicklung produktiver MAUI-Apps hatte ich die Gelegenheit, alle vier hier behandelten Ansätze in der Praxis zu testen. Schauen wir sie uns an.
SQLite — das klassische Fundament
SQLite ist der De-facto-Standard für lokale Speicherung auf Mobilgeräten. Mit sqlite-net-pcl erhält man eine leichtgewichtige, konfigurationsfreie eingebettete Datenbank:
Minimaler Footprint — die gesamte Engine ist eine einzige Datei
Plattformübergreifend — funktioniert identisch auf Android, iOS, Windows und macOS
SQL-basiert — vertraute Abfragen für alle, die relationale Datenbanken kennen
Ausgereift und bewährt — wird auf Milliarden von Geräten weltweit eingesetzt
public class ArticleRepository
{
private readonly SQLiteAsyncConnection _db;
public ArticleRepository(string dbPath)
{
_db = new SQLiteAsyncConnection(dbPath);
_db.CreateTableAsync<Article>().Wait();
}
public Task<List<Article>> GetAllAsync()
=> _db.Table<Article>().ToListAsync();
public Task<Article> GetByIdAsync(int id)
=> _db.Table<Article>()
.Where(a => a.Id == id)
.FirstOrDefaultAsync();
public Task SaveAsync(Article article)
=> article.Id == 0
? _db.InsertAsync(article)
: _db.UpdateAsync(article);
}Wann SQLite wählen?
SQLite ist ideal für einfache bis mittlere Datenmodelle, bei denen man volle Kontrolle über Abfragen, minimale Abhängigkeiten und maximale Portabilität wünscht. Es ist die erste Wahl, wenn das Datenschema unkompliziert ist und keine erweiterten ORM-Funktionen benötigt werden.
Entity Framework Core — das vollständige ORM
EF Core bringt die volle Kraft eines ORM mit seinem SQLite-Provider nach .NET MAUI:
Code-First-Ansatz — Modelle in C# definieren und EF das Schema generieren lassen
LINQ-Abfragen — typsichere, zur Kompilierzeit geprüfte Abfragen
Change Tracking — automatische Dirty Detection und Batch-Updates
Migrationen — Datenbankschema zusammen mit dem Code versionieren
public class AppDbContext : DbContext
{
public DbSet<Article> Articles => Set<Article>();
public DbSet<Tag> Tags => Set<Tag>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Article>()
.HasMany(a => a.Tags)
.WithMany(t => t.Articles);
}
}
var recentArticles = await _db.Articles
.Include(a => a.Tags)
.Where(a => a.PublishedAt > DateTime.UtcNow.AddDays(-30))
.OrderByDescending(a => a.PublishedAt)
.Take(10)
.ToListAsync();Wann Entity Framework Core wählen?
EF Core glänzt bei komplexen relationalen Datenmodellen mit Many-to-Many-Beziehungen, wenn Change Tracking benötigt wird oder Modelldefinitionen zwischen MAUI-Client und ASP.NET-Backend geteilt werden sollen.
Realm — die Objektdatenbank
Realm verfolgt einen grundlegend anderen Ansatz — es ist eine Objektdatenbank, keine relationale:
Zero-Copy-Architektur — Objekte werden direkt aus der Datenbankdatei gelesen, keine Deserialisierung
Reaktiv — eingebaute Änderungsbenachrichtigungen über INotifyPropertyChanged
Thread-sicher — automatische Thread-Beschränkung mit Cross-Thread-Zugriffsmustern
Sync-fähig — Atlas Device Sync für Cloud-Synchronisation (optional)
public partial class Article : IRealmObject
{
[PrimaryKey]
[MapTo("_id")]
public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTimeOffset PublishedAt { get; set; }
public IList<Tag> Tags { get; } = null!;
}
var recentArticles = _realm.All<Article>()
.Where(a => a.PublishedAt > DateTimeOffset.UtcNow.AddDays(-30))
.OrderByDescending(a => a.PublishedAt);
_realm.Write(() =>
{
_realm.Add(new Article
{
Title = "Neuer Beitrag",
Content = "Hallo, Welt!",
PublishedAt = DateTimeOffset.UtcNow
});
});Wann Realm wählen?
Realm glänzt bei Apps, die reaktive UI-Updates in Echtzeit, Offline-First-Synchronisation mit einem Cloud-Backend benötigen oder bei tief verschachtelten Objektgraphen, die in einem relationalen Modell umständlich wären.
Dapper — das Micro-ORM
Dapper liegt zwischen rohem SQLite und einem vollständigen ORM. Es mappt SQL-Ergebnisse mit minimalem Overhead auf Objekte:
Rohes SQL mit Mapping — Sie schreiben die Abfragen, Dapper mappt die Ergebnisse
Extrem schnell — praktisch kein Overhead im Vergleich zu rohem ADO.NET
Leichtgewichtig — ein einziges NuGet-Paket, keine komplexe Konfiguration
Flexibel — Stored Procedures, Multi-Mapping, dynamische Parameter
public class ArticleRepository
{
private readonly IDbConnection _connection;
public ArticleRepository(string dbPath)
{
_connection = new SqliteConnection($"Data Source={dbPath}");
}
public async Task<IEnumerable<Article>> GetRecentAsync()
{
const string sql = @"
SELECT a.*, t.Name as TagName
FROM Articles a
LEFT JOIN ArticleTags at ON a.Id = at.ArticleId
LEFT JOIN Tags t ON at.TagId = t.Id
WHERE a.PublishedAt > @Since
ORDER BY a.PublishedAt DESC";
return await _connection.QueryAsync<Article>(
sql,
new { Since = DateTime.UtcNow.AddDays(-30) });
}
public async Task<int> InsertAsync(Article article)
{
const string sql = @"
INSERT INTO Articles (Title, Content, PublishedAt)
VALUES (@Title, @Content, @PublishedAt);
SELECT last_insert_rowid();";
return await _connection.ExecuteScalarAsync<int>(sql, article);
}
}Wann Dapper wählen?
Dapper ist perfekt, wenn man volle Kontrolle über SQL mit dem Komfort des automatischen Objekt-Mappings wünscht. Es eignet sich hervorragend für leistungskritische Pfade, komplexe Abfragen, mit denen ORMs Schwierigkeiten haben, oder Teams, die SQL gegenüber LINQ bevorzugen.
Vergleich
| Eigenschaft | SQLite (roh) | Entity Framework | Realm | Dapper |
|---|---|---|---|---|
| Typ | Eingebettete DB | Vollständiges ORM | Objekt-DB | Micro-ORM |
| Abfragesprache | SQL | LINQ | LINQ | SQL |
| Leistung | Sehr hoch | Mittel | Sehr hoch | Sehr hoch |
| Lernkurve | Niedrig | Mittel | Mittel | Niedrig |
| Change Tracking | Nein | Ja | Ja | Nein |
| Reaktive Updates | Nein | Nein | Ja | Nein |
| Cloud-Sync | Nein | Nein | Ja (Atlas) | Nein |
| Paketgröße | Klein | Groß | Mittel | Klein |
Leistungstests
In meinen Tests mit 10.000 Datensätzen auf einem Mittelklasse-Android-Gerät:
| Operation | SQLite | EF Core | Realm | Dapper |
|---|---|---|---|---|
| Insert (Batch) | 120ms | 450ms | 95ms | 130ms |
| Alle lesen | 45ms | 180ms | 30ms | 50ms |
| Abfrage (gefiltert) | 12ms | 85ms | 8ms | 15ms |
| Update (einzeln) | 3ms | 25ms | 2ms | 4ms |
Hinweis: Dies sind ungefähre Werte — die tatsächliche Leistung hängt von der Datenkomplexität, dem Gerät und dem Schema-Design ab.
Zusammenfassung
Meine Empfehlung nach Projekttyp:
Einfache Apps mit grundlegender Speicherung → SQLite mit sqlite-net — minimales Setup, erledigt die Aufgabe
Komplexe relationale Daten + gemeinsame Backend-Modelle → Entity Framework Core — LINQ-Power, Migrationen, vertraute API
Offline-First-Apps mit Echtzeit-Sync → Realm — reaktive Updates, Cloud-Sync, exzellente Mobile-Performance
Leistungskritische Apps mit SQL-Kontrolle → Dapper — nahezu null Overhead, volle SQL-Flexibilität
In der Praxis habe ich oft Ansätze kombiniert: SQLite/Dapper für leichtgewichtiges Caching neben EF Core für das Hauptdatenmodell in derselben Anwendung. Scheuen Sie sich nicht, je nach Anforderung zu mischen.
