Skip to content

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.

Paweł Rudnicki
10 min
  • .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

FeatureSQLite (raw)Entity FrameworkRealmDapper
TypeEmbedded DBFull ORMObject DBMicro-ORM
Query languageSQLLINQLINQSQL
PerformanceVery highMediumVery highVery high
Learning curveLowMediumMediumLow
Change trackingNoYesYesNo
Reactive updatesNoNoYesNo
Cloud syncNoNoYes (Atlas)No
Package sizeSmallLargeMediumSmall

Performance Benchmarks

In my tests with 10,000 records on a mid-range Android device:

OperationSQLiteEF CoreRealmDapper
Insert (batch)120ms450ms95ms130ms
Read all45ms180ms30ms50ms
Query (filtered)12ms85ms8ms15ms
Update (single)3ms25ms2ms4ms

Note: these are approximate values — real performance depends on data complexity, device and schema design.

Summary

My recommendation based on project type:

  1. Simple apps with basic storage → SQLite with sqlite-net — minimal setup, gets the job done

  2. Complex relational data + shared backend models → Entity Framework Core — LINQ power, migrations, familiar API

  3. Offline-first apps with real-time sync → Realm — reactive updates, cloud sync, excellent mobile performance

  4. 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.

Let's talk about the planned project

The first consultation is free and without obligation. A short description of the process and the expected outcome is enough.

Let's talk about the project