Lokal Database i .NET MAUI: Komplet Guide til SQLite og Entity Framework Core (2026)

Lær at bygge en hurtig, sikker og vedligeholdbar lokal database i .NET MAUI med enten sqlite-net-pcl eller Entity Framework Core 9. Inkluderer setup, repository-mønster, migrationer, SQLCipher-kryptering og performance-tips med fungerende C#-eksempler.

.NET MAUI SQLite & EF Core Guide (2026)

Opdateret: 23. juni 2026

En lokal database i .NET MAUI gemmer struktureret data direkte på enheden, så din app fungerer offline og indlæser data hurtigere end et netværkskald. Den mest anvendte løsning er sqlite-net-pcl til lette CRUD-scenarier og Entity Framework Core 9 når du har brug for relationer, migrationer og LINQ. Begge biblioteker gemmer en .db-fil i appens private datamappe og kører fuldt cross-platform på iOS, Android, Windows og macOS uden ekstra opsætning.

  • sqlite-net-pcl er det letteste valg til simple tabeller. Én NuGet-pakke, ingen kodegenerering og asynkrone CRUD-metoder ud af boksen.
  • Entity Framework Core 9 er bedre, når du har relationer, navigation properties og brug for skema-migrationer. Til gengæld er det ~10 MB større og langsommere ved første start.
  • Læg altid databasefilen under FileSystem.Current.AppDataDirectory. Den er privat per app og kommer med i backup på iOS og Android.
  • Brug SQLCipher (via SQLitePCLRaw bundle) til at kryptere database-filen, når den indeholder personhenførbare data, kombineret med en nøgle gemt i SecureStorage.
  • Registrer databasen som Singleton i din DI-container. Åbn forbindelsen én gang, ikke pr. forespørgsel. Det er den hyppigste performance-fejl jeg ser i MAUI-apps under code reviews.
  • Async hele vejen: SaveChangesAsync, ToListAsync og InsertAsync blokerer ikke UI-tråden og er obligatoriske på mobil.

Hvilket bibliotek skal du vælge?

Valget mellem sqlite-net-pcl og EF Core er ærligt talt den vigtigste arkitektoniske beslutning, du tager, før du skriver en eneste linje data-kode. De løser samme problem på vidt forskellige måder, og hvis du vælger forkert, betaler du med enten kompleksitet eller manglende funktionalitet senere i projektet. I praksis falder valget på et af tre kriterier: størrelsen på dit datasæt, kompleksiteten af dine relationer, og hvor ofte dit skema skal ændre sig efter lancering.

Egenskabsqlite-net-pcl 1.9EF Core 9
App-størrelse (tilføjet)~1.2 MB~9-11 MB
Cold start overhead~5 ms~80-150 ms
LINQ-understøttelseBegrænset (Where, OrderBy)Fuld LINQ-to-SQL
MigrationerManuel SQLIndbygget dotnet ef migrations
RelationerIngen (manuelle joins)Navigation properties
Async APIJa (SQLiteAsyncConnection)Ja (SaveChangesAsync)
LæringskurveLavMedium-høj
Bedst tilIndstillinger, cache, små listerDomænemodeller med relationer

For de fleste apps med under fem tabeller og ingen relationer er sqlite-net-pcl det rigtige valg. Den koster næsten intet i app-størrelse og er hurtigere at komme i gang med. Skifter du senere til EF Core, kan du genbruge selve .db-filen, fordi formatet er identisk. Hvis du derimod bygger en feltservice-app med kunder, ordrer, fakturalinjer og fotos, der skal synkroniseres, sparer EF Core dig for hundredvis af linjers join-kode og giver dig migrationer gratis.

Opsæt sqlite-net-pcl i .NET MAUI

Så lad os komme i gang. Det hurtigste setup tager under fem minutter. Installer NuGet-pakken, opret en model med attributter, og åbn en asynkron forbindelse mod en sti i appens private datamappe. Brug altid FileSystem.Current.AppDataDirectory. Det er den eneste sti, der virker uændret på tværs af iOS, Android, Windows og macOS, og som overlever app-opdateringer.

// 1) Installer pakker via .csproj eller dotnet add package
// <PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
// <PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.10" />

using SQLite;

public class TodoItem
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }

    [MaxLength(200), NotNull]
    public string Title { get; set; } = string.Empty;

    [Indexed]
    public DateTime CreatedAt { get; set; }

    public bool IsDone { get; set; }
}

public sealed class TodoDatabase
{
    private readonly SQLiteAsyncConnection _db;

    public TodoDatabase()
    {
        var path = Path.Combine(FileSystem.Current.AppDataDirectory, "todo.db3");
        _db = new SQLiteAsyncConnection(path,
            SQLiteOpenFlags.ReadWrite |
            SQLiteOpenFlags.Create |
            SQLiteOpenFlags.SharedCache);
    }

    public Task InitAsync() => _db.CreateTableAsync<TodoItem>();

    public Task<List<TodoItem>> GetAllAsync() =>
        _db.Table<TodoItem>().OrderByDescending(t => t.CreatedAt).ToListAsync();

    public Task<int> SaveAsync(TodoItem item) =>
        item.Id == 0 ? _db.InsertAsync(item) : _db.UpdateAsync(item);

    public Task<int> DeleteAsync(TodoItem item) => _db.DeleteAsync(item);
}

Registrer derefter TodoDatabase som singleton i MauiProgram.cs. En typisk fejl er at lave en ny instans pr. ViewModel, hvilket åbner og lukker forbindelsen gentagne gange og dræber performance (jeg har selv brændt mig på den ved et tidligt MAUI-projekt). Vores guide til dependency injection i .NET MAUI gennemgår, hvorfor singleton-lifetime er den rigtige strategi for dataadgangsklasser.

Opsæt Entity Framework Core 9

EF Core 9 understøtter .NET MAUI fuldt ud, så du behøver ikke længere de tidligere workarounds fra 2023. Tilføj de tre nødvendige pakker, lav en DbContext og pas på én ting: AOT-compilation på iOS kræver, at du sætter UseSqlite med en eksplicit type-mapping, ellers fejler appen ved første kald.

// <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
// <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" PrivateAssets="all" />

using Microsoft.EntityFrameworkCore;

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Email { get; set; }
    public List<Order> Orders { get; set; } = new();
}

public class Order
{
    public int Id { get; set; }
    public DateTime PlacedAt { get; set; }
    public decimal Total { get; set; }
    public int CustomerId { get; set; }
    public Customer Customer { get; set; } = null!;
}

public sealed class AppDbContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        var path = Path.Combine(FileSystem.Current.AppDataDirectory, "app.db");
        options.UseSqlite($"Filename={path}");
    }

    protected override void OnModelCreating(ModelBuilder mb)
    {
        mb.Entity<Customer>().HasIndex(c => c.Email).IsUnique();
        mb.Entity<Order>()
          .HasOne(o => o.Customer)
          .WithMany(c => c.Orders)
          .OnDelete(DeleteBehavior.Cascade);
    }
}

Bemærk, at vi bruger options.UseSqlite med en sti og ikke en åben forbindelse. EF Core 9 håndterer connection pooling internt, og du skal undgå at gøre det selv. Registrer derefter konteksten som AddDbContext med scoped lifetime, hvis du følger MVVM stramt, ellers transient. For en dybere gennemgang af model-binding og kommandoer, se vores guide til MVVM-arkitektur i .NET MAUI.

Repository-mønster og dependency injection

Direkte brug af DbContext eller SQLiteAsyncConnection fra dine ViewModels gør koden umulig at unit-teste og binder dig til ét datalag for altid. Repository-mønsteret løser begge problemer ved at lægge et interface foran databasen. Det lyder lidt gammeldags i 2026, men i mobil-sammenhæng (hvor du oftere bytter SQLite ud med en sky-API eller en cache) er det stadig det enkleste mønster, der virker.

public interface ICustomerRepository
{
    Task<IReadOnlyList<Customer>> GetAllAsync(CancellationToken ct = default);
    Task<Customer?> FindAsync(int id, CancellationToken ct = default);
    Task UpsertAsync(Customer customer, CancellationToken ct = default);
    Task DeleteAsync(int id, CancellationToken ct = default);
}

public sealed class CustomerRepository : ICustomerRepository
{
    private readonly IDbContextFactory<AppDbContext> _factory;

    public CustomerRepository(IDbContextFactory<AppDbContext> factory) =>
        _factory = factory;

    public async Task<IReadOnlyList<Customer>> GetAllAsync(CancellationToken ct = default)
    {
        await using var db = await _factory.CreateDbContextAsync(ct);
        return await db.Customers.AsNoTracking()
                                 .OrderBy(c => c.Name)
                                 .ToListAsync(ct);
    }

    public async Task UpsertAsync(Customer customer, CancellationToken ct = default)
    {
        await using var db = await _factory.CreateDbContextAsync(ct);
        if (customer.Id == 0) db.Customers.Add(customer);
        else db.Customers.Update(customer);
        await db.SaveChangesAsync(ct);
    }

    // ... øvrige metoder udeladt for læselighed
}

// I MauiProgram.cs:
builder.Services.AddDbContextFactory<AppDbContext>();
builder.Services.AddSingleton<ICustomerRepository, CustomerRepository>();

Brug IDbContextFactory<T> i stedet for at injecte selve DbContext. En kontekst er ikke trådsikker, og MAUI-apps har ofte parallelle baggrundsopgaver, der ellers vil crashe med "A second operation was started on this context". Jeg ramte præcis den fejl, da jeg første gang shippede en baggrundssynkronisering. Sætter du AsNoTracking på alle læseforespørgsler, sparer du desuden 20-40% hukommelse på lange lister, fordi EF Core ikke holder change-tracking-snapshots.

Hvordan håndterer du database-migrationer?

Migrationer er det område, hvor EF Core klart vinder over sqlite-net-pcl. Når du udgiver version 2.0 af din app, kan brugerens enhed stadig have skema fra 1.7, og du skal kunne tilføje kolonner, omdøbe tabeller og kopiere data uden at miste noget. EF Core gør det med dotnet ef migrations add, mens du i sqlite-net-pcl selv skriver ALTER TABLE sætninger og holder styr på versionsnummeret.

# Tilføj en migration når du ændrer modellen
dotnet ef migrations add AddCustomerPhone --project MyApp

# Generér SQL-script til review (ikke kør på enheden)
dotnet ef migrations script --output migration.sql

# Anvend migrationer ved app-start
public static class DatabaseInitializer
{
    public static async Task MigrateAsync(IServiceProvider services)
    {
        await using var scope = services.CreateAsyncScope();
        var factory = scope.ServiceProvider
                           .GetRequiredService<IDbContextFactory<AppDbContext>>();
        await using var db = await factory.CreateDbContextAsync();
        await db.Database.MigrateAsync();
    }
}

For sqlite-net-pcl er det mest pålidelige mønster en simpel version-tabel og en switch på app-start. Hvis dit projekt vokser ud af det, er det et stærkt signal om, at det er tid til at skifte til EF Core.

Sådan krypterer du databasen med SQLCipher

SQLite-filer gemmes som ren tekst på disken, og en jailbreaket iOS-enhed eller en Android-enhed med USB-debugging kan læse hele indholdet med et almindeligt SQLite-værktøj. Hvis databasen indeholder personhenførbare data, finansdata eller helbredsoplysninger, skal du kryptere den. I 2026 er det også et krav under NIS2-direktivet for visse brancher.

// Skift bundlen ud i .csproj:
// <PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="2.1.10" />

using SQLite;

public sealed class SecureTodoDatabase
{
    private readonly SQLiteAsyncConnection _db;

    public SecureTodoDatabase(string key)
    {
        var path = Path.Combine(FileSystem.Current.AppDataDirectory, "secure.db3");
        var opts = new SQLiteConnectionString(path, true, key: key);
        _db = new SQLiteAsyncConnection(opts);
    }
}

// Generér og gem nøglen i SecureStorage ved første start
public static async Task<string> GetOrCreateDbKeyAsync()
{
    var existing = await SecureStorage.Default.GetAsync("db_key");
    if (existing is not null) return existing;

    var bytes = new byte[32];
    RandomNumberGenerator.Fill(bytes);
    var key = Convert.ToBase64String(bytes);
    await SecureStorage.Default.SetAsync("db_key", key);
    return key;
}

Nøglen ligger nu i iOS Keychain eller Android Keystore (den hardware-bakkede secure enclave), og databasen er ubrugelig uden den. Læs mere om sikker opbevaring af nøgler i vores guide til sikkerhed i .NET MAUI, herunder hvordan du kombinerer databasens nøgle med biometrisk login for ekstra beskyttelse.

Performance: undgå de fem klassiske fejl

De fleste databaseproblemer i MAUI-apps stammer fra en håndfuld gentagne fejl, som er nemme at undgå, hvis du kender dem. Her er de fem, vi konsekvent ser i kode-reviews, og hvordan du fikser dem.

  1. Åbning og lukning pr. forespørgsel. En SQLite-forbindelse koster 5-20 ms at åbne. Læg den i en singleton eller brug IDbContextFactory. Aldrig using var db = new ... i en løkke.
  2. Manglende indekser. En WHERE-forespørgsel på en ikke-indekseret kolonne med 10.000 rækker tager 200+ ms. Annotér med [Indexed] i sqlite-net-pcl eller HasIndex i EF Core på alle felter, du filtrerer på.
  3. Batch-insert uden transaktion. 1.000 individuelle InsertAsync-kald tager ~8 sekunder. Wrap dem i RunInTransactionAsync eller én SaveChangesAsync, så bliver det 100 ms.
  4. Change tracking på read-only data. EF Core holder en snapshot af alle indlæste entiteter. Kald AsNoTracking() når du kun læser, og spar 30-50% hukommelse.
  5. Synkrone kald på UI-tråden. Table<T>().ToList() blokerer hovedtråden. Brug altid ToListAsync eller du får ANR-dialoger på Android.

Vores artikel om ydeevneoptimering i .NET MAUI går i dybden med profilering, og hvordan du opdager disse problemer med dotnet-trace og Android Studio Profiler.

Test og debug af din lokale database

Lokal database er nem at unit-teste, hvis du har gjort dit hjemmearbejde med repository-mønsteret. EF Core understøtter en in-memory SQLite-provider, ikke at forveksle med UseInMemoryDatabase, som ikke håndhæver foreign keys og giver falske grønne tests. Den korrekte tilgang er en SQLite-forbindelse mod ":memory:":

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

[Test]
public async Task UpsertAsync_NewCustomer_SetsId()
{
    var connection = new SqliteConnection("Filename=:memory:");
    await connection.OpenAsync();

    var options = new DbContextOptionsBuilder<AppDbContext>()
        .UseSqlite(connection)
        .Options;

    await using (var ctx = new AppDbContext(options))
        await ctx.Database.EnsureCreatedAsync();

    var factory = new TestDbContextFactory(options);
    var repo = new CustomerRepository(factory);

    await repo.UpsertAsync(new Customer { Name = "Anders" });

    var all = await repo.GetAllAsync();
    Assert.That(all, Has.Count.EqualTo(1));
}

For at inspicere databasen på en fysisk enhed kan du i Android pull'e filen med adb pull /data/data/com.dinapp/files/app.db (kræver debug-build) og åbne den i DB Browser for SQLite. På iOS finder du filen under Devices and Simulators → Installed Apps → Download Container i Xcode. Mere om automatiseret test af MAUI-apps står i vores guide til teststrategi for .NET MAUI.

Ofte stillede spørgsmål

Hvor gemmes SQLite-filen i en .NET MAUI app?

Standard er FileSystem.Current.AppDataDirectory, som på iOS mapper til appens Library-folder og på Android til /data/data/<package>/files. Filen er privat for din app, indgår i automatisk backup og overlever app-opdateringer, men ikke afinstallation.

Kan jeg dele en SQLite-database mellem iOS og Android?

Ja, database-formatet er identisk på tværs af platforme. Du kan generere en seed-database på din udviklingsmaskine, lægge den som MauiAsset, kopiere den til AppDataDirectory ved første start og bruge samme fil på begge OS uden ændringer.

Skal jeg bruge sqlite-net-pcl eller EF Core i 2026?

Vælg sqlite-net-pcl hvis du har under fem tabeller uden relationer og prioriterer lille app-størrelse. Vælg EF Core 9 hvis du har relationelle data, LINQ-tunge forespørgsler eller skal udgive skema-ændringer over tid. Begge er fuldt supporterede.

Hvordan synkroniserer jeg en lokal SQLite med en sky-backend?

Det mest pålidelige mønster er en last-write-wins sync med en UpdatedAt-kolonne og en SyncStatus-enum (Pending, Synced, Conflict). Push lokale ændringer først, pull derefter serverændringer, og brug en eksponentiel backoff ved netværksfejl. SQLite-Sync og Microsofts Datasync Community Toolkit er begge gode startpunkter.

Er SQLite hurtig nok til 100.000+ rækker på mobil?

Ja, SQLite kan håndtere flere millioner rækker, hvis du indekserer korrekt og bruger transaktioner ved batch-insert. Flaskehalsen er næsten altid manglende indekser eller change tracking på read-only data, ikke selve engine'en. Mål med EXPLAIN QUERY PLAN før du optimerer.

Editorial Team
Om Forfatteren Editorial Team

Our team of expert writers and editors.