Databases & ORMs in .NET MAUI — Realm, Entity Framework, SQLite and Dapper Compared
A practical comparison of four popular data persistence approaches in .NET MAUI applications. Performance, developer experience and use-case recommendations.
- .NET MAUI
- SQLite
- Entity Framework
- Realm
- Dapper
Introduction
Every mobile application needs to store data locally — whether it's user settings, cached API responses or full offline-first datasets. In the .NET MAUI ecosystem, we have several mature options, each with distinct trade-offs.
After years of building production MAUI apps, I've had the chance to battle-test all four approaches covered here. Let's break them down.
SQLite — the Classic Foundation
SQLite is the de facto standard for local storage on mobile. With sqlite-net-pcl, you get a lightweight, zero-configuration embedded database:
Tiny footprint — the entire engine is a single file
Cross-platform — works identically on Android, iOS, Windows and macOS
SQL-based — familiar queries for anyone who knows relational databases
Mature and battle-tested — used in billions of devices worldwide
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);
}When to choose SQLite?
SQLite is ideal for simple to medium data models where you want full control over queries, minimal dependencies and maximum portability. It's the go-to choice when your data schema is straightforward and you don't need advanced ORM features.
Entity Framework Core — the Full ORM
EF Core brings the full power of an ORM to .NET MAUI with its SQLite provider:
Code-first approach — define models in C# and let EF generate the schema
LINQ queries — type-safe, compile-time checked queries
Change tracking — automatic dirty detection and batch updates
Migrations — version your database schema alongside your code
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();When to choose Entity Framework Core?
EF Core shines when you have complex relational data models with many-to-many relationships, need change tracking, or want to share model definitions between your MAUI client and an ASP.NET backend.
Realm — the Object Database
Realm takes a fundamentally different approach — it's an object database, not a relational one:
Zero-copy architecture — objects are accessed directly from the database file, no deserialization
Reactive — built-in change notifications via INotifyPropertyChanged
Thread-safe — automatic thread confinement with cross-thread access patterns
Sync-ready — Atlas Device Sync for cloud synchronization (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 = "New Post",
Content = "Hello, world!",
PublishedAt = DateTimeOffset.UtcNow
});
});When to choose Realm?
Realm excels in apps that need real-time reactive UI updates, offline-first synchronization with a cloud backend, or when dealing with deeply nested object graphs that would be cumbersome in a relational model.
Dapper — the Micro-ORM
Dapper sits between raw SQLite and a full ORM. It maps SQL results to objects with minimal overhead:
Raw SQL with mapping — you write the queries, Dapper maps the results
Blazing fast — practically no overhead compared to raw ADO.NET
Lightweight — a single NuGet package, no complex configuration
Flexible — stored procedures, multi-mapping, dynamic parameters
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);
}
}When to choose Dapper?
Dapper is perfect when you want full control over SQL with the convenience of automatic object mapping. It's great for performance-critical paths, complex queries that ORMs struggle with, or teams who prefer writing SQL over LINQ.
Comparison
| Feature | SQLite (raw) | Entity Framework | Realm | Dapper |
|---|---|---|---|---|
| Type | Embedded DB | Full ORM | Object DB | Micro-ORM |
| Query language | SQL | LINQ | LINQ | SQL |
| Performance | Very high | Medium | Very high | Very high |
| Learning curve | Low | Medium | Medium | Low |
| Change tracking | No | Yes | Yes | No |
| Reactive updates | No | No | Yes | No |
| Cloud sync | No | No | Yes (Atlas) | No |
| Package size | Small | Large | Medium | Small |
Performance Benchmarks
In my tests with 10,000 records on a mid-range Android device:
| Operation | SQLite | EF Core | Realm | Dapper |
|---|---|---|---|---|
| Insert (batch) | 120ms | 450ms | 95ms | 130ms |
| Read all | 45ms | 180ms | 30ms | 50ms |
| Query (filtered) | 12ms | 85ms | 8ms | 15ms |
| Update (single) | 3ms | 25ms | 2ms | 4ms |
Note: these are approximate values — real performance depends on data complexity, device and schema design.
Summary
My recommendation based on project type:
Simple apps with basic storage → SQLite with sqlite-net — minimal setup, gets the job done
Complex relational data + shared backend models → Entity Framework Core — LINQ power, migrations, familiar API
Offline-first apps with real-time sync → Realm — reactive updates, cloud sync, excellent mobile performance
Performance-critical apps where you want SQL control → Dapper — near-zero overhead, full SQL flexibility
In practice, I've often combined approaches: SQLite/Dapper for lightweight caching alongside EF Core for the main data model in the same application. Don't be afraid to mix and match based on each feature's needs.
