Skip to content
Nuvyntra Labs

What it is

LocalStore is an abstract database layer for .NET MAUI on Android, iOS, Mac Catalyst, and Windows. The host picks an engine (StoreBackend). Application code always uses the same methods on ILocalStore / IStoreCollection<T>. You do not change insert / find / replace / delete / select when you add or switch a backend. Each engine has its own file. Set AutoMigrate and Map<T> to copy collections when you switch. Raw SQL or NQL runs on ILocalStore.QueryAsync. Optional [StoreDao] interfaces are source-generated.

host always calls
  InsertAsync / InsertManyAsync / FindByIdAsync / ReplaceAsync / DeleteByIdAsync / FindAsync
                    ↓
              IStoreCollection<T>
     ┌────────────┼────────────┐
  SQLite      NuvexaDB     Realm / LiteDB / DuckDB
  SQLCipher   Firebird     LMDB / RocksDB / LevelDB

Local / embedded databases

1.1 opens every engine in the table. Host CRUD stays the same. NuvexaDB is Nuventra.NuvexaDB. Every row is reached through the same IStoreCollection<T> methods.

DatabaseTypeDefault pathBest forStatus
SQLiteRelationalapp.dbGeneral-purpose local DBShipped
NuvexaDBDocument NoSQLapp.nvxEmbedded .nvx documentsShipped
RealmObject DBapp.realmMobile / offline-firstShipped
LiteDBDocument NoSQLapp.litedbEmbedded NoSQLShipped
DuckDBAnalytical SQLapp.duckdbAnalytics / OLAPShipped (JSON fallback on mobile)
SQLCipherEncrypted SQLiteapp.dbSecure local DBShipped
Firebird EmbeddedRelationalapp.fdbMore advanced relational DBShipped (JSON fallback without fbembed)
LMDBKey-valueapp.lmdb/Very fast key-value storageShipped
RocksDBKey-valueapp.rocksdb/High-performance storageShipped (JSON fallback on mobile)
LevelDBKey-valueapp.leveldb/Simple KV storageShipped (JSON fallback when native is missing)

Platforms

LocalStore targets Android, iOS, Windows, and Mac Catalyst. You set StoreBackend the same way on every OS. Yes = that database actually runs. JSON fallback = the NuGet has no native library for that OS, so LocalStore does not use the real engine.

DatabaseAndroidiOSWindowsMac Catalyst
SQLiteYesYesYesYes
SQLCipherYesYesYesYes
NuvexaDBYesYesYesYes
LiteDBYesYesYesYes
RealmYesYesYesYes
LMDBYesYesYesJSON fallback
DuckDBJSON fallbackJSON fallbackYes (win-x64, win-arm64)JSON fallback
FirebirdJSON fallbackJSON fallbackYes (Embedded NuGet)JSON fallback
RocksDBJSON fallbackJSON fallbackYes (win-x64 only)JSON fallback
LevelDBJSON fallbackJSON fallbackJSON fallbackJSON fallback

Selection does not change on an unsupported platform. Keep o.Backend = StoreBackend.DuckDb (or Firebird, RocksDB, LevelDB, or LMDB on Catalyst). LocalStore.Open / UseMauiLocalStore does not throw. On that OS, each row is a JSON file. InsertAsync, FindByIdAsync, ReplaceAsync, DeleteByIdAsync, and FindAsync still work. Filters run in memory. EnsureIndexAsync does nothing. QueryLanguage is None.

Common methods

Portable CRUD stays on IStoreCollection<T>. Raw SQL / NQL is optional on ILocalStore. Every backend must implement the collection operations:

OperationMethod
CreateInsertAsync / InsertManyAsync
ReadFindByIdAsync
UpdateReplaceAsync
DeleteDeleteByIdAsync
SelectFindAsync(StoreFilter, StoreQuery)
Raw SQL / NQLILocalStore.QueryAsync<T> / ExecuteAsync
Generated DAOstore.GetDao<T>()
IndexEnsureIndexAsync
CloseDisposeAsync

POCOs need a public string Id (nullable is fine) and a public parameterless constructor. Filters use top-level property names (Age, City). Get-only or [JsonIgnore] members are not stored. Scalar types: string, int, long, double, float, bool, DateTime.

Options

LocalStoreOptions.Backend defaults to StoreBackend.Nuvexa. Set it explicitly when you want SQLite or another engine. When Path is empty, the file is LocalApplicationData/Plugin.Maui.LocalStore/ plus the default path in the engine table.

OptionDefaultUsed by
BackendNuvexaAll
Pathsee ResolvePathAll (file or directory, per engine)
CreateIfMissingtrueAll. false throws LocalStoreException if the file is missing
EncryptionKeynoneNuvexa (required to open an encrypted .nvx), SQLCipher (required), LiteDB password, Realm, Firebird SYSDBA password. Ignored by SQLite, DuckDB, LMDB, RocksDB, LevelDB
CacheSizeMb16Nuvexa only
AutoMigratefalseCopy Map<T> collections from another engine file when the destination is empty
MigrateFromnoneSource engine. Required when more than one sibling file exists
MigrateFromPathnoneSource file. Empty uses the destination folder
MigrateFromEncryptionKeynoneSource key. Empty reuses EncryptionKey
DeleteSourceAfterMigratefalseRemove the source file after a successful copy
Map<T>(name)noneRegisters a collection for migrate (required when AutoMigrate is true)

Engine NuGet references

BackendPackage
SQLitesqlite-net-base, SQLitePCLRaw.bundle_e_sqlite3
SQLCiphersame mapping + SQLitePCLRaw.bundle_e_sqlcipher (do not also reference sqlite-net-sqlcipher)
NuvexaDBNuventra.NuvexaDB 1.0.6
LiteDBLiteDB
RealmRealm
DuckDBDuckDB.NET.Data.Full (desktop natives)
FirebirdFirebirdSql.Data.FirebirdClient, FirebirdDb.Embedded.V5.NativeAssets.Windows.All, FirebirdDb.Embedded.V5.NativeAssets.Linux.All
LMDBLightningDB
RocksDBRocksDB (desktop natives)
LevelDBLevelDB.Standard with ExcludeAssets=native;build;buildTransitive

Raw SQL / NQL

ILocalStore.QueryAsync<T> / ExecuteAsync are on the shared store. The dialect is store.QueryLanguage. JSON fallback (mobile DuckDB / Firebird / RocksDB / LevelDB, and LMDB on Catalyst) reports None.

EngineQueryLanguageCommand
SQLite, SQLCipherSqlSQL
DuckDB, FirebirdSql when native; None on the JSON fallbackSQL when native
NuvexaNqlNQL
LiteDB, Realm, LMDB, RocksDB, LevelDBNonethrows LocalStoreException
if (store.QueryLanguage == StoreQueryLanguage.Sql)
{
    var adults = await store.QueryAsync<Person>(
        "SELECT * FROM users WHERE Age >= ?",
        [21]);
}

if (store.QueryLanguage == StoreQueryLanguage.Nql)
{
    var adults = await store.QueryAsync<Person>(
        """db.users.find({ age: { $gte: 21 } }).sort({ name: 1 }).limit(20)""");
}

INuvexaLocalStore.ExecuteNqlAsync still returns raw JSON strings. Prefer QueryAsync<T> when you want POCOs. NQL update / delete needs NuvexaDB 1.0.2+.

Engine query language when QueryLanguage is Nql. NuvexaDB NQL

Automatic migration

Each engine keeps its own file. On open, LocalStore can copy registered collections when the destination is empty. Map<T> is required so both engines can read and write the same POCOs. If MigrateFrom is omitted and exactly one other engine file sits next to the destination, that file is used. Two or more siblings throw until you set MigrateFrom.

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.Nuvexa;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.nvx");
    o.EncryptionKey = key;
    o.AutoMigrate = true;
    o.MigrateFrom = StoreBackend.Sqlite;
    o.Map<Person>("users");
});

Or copy without changing LocalStore.Current:

var result = await LocalStore.MigrateAsync(
    new LocalStoreOptions { Backend = StoreBackend.Sqlite, Path = sqlitePath },
    new LocalStoreOptions { Backend = StoreBackend.Nuvexa, Path = nvxPath, EncryptionKey = key }
        .Map<Person>("users"));

Destination rows win: a non-empty mapped collection is left unchanged (Skipped). JSON-fallback folders migrate the same way as native files. StoreMigrationResult reports From, To, Collections, Documents, Skipped, and Reason.

Source-generated DAOs

[StoreDao] marks an interface. The generator emits an implementation that wraps IStoreCollection<T> and QueryAsync. CRUD method names map to the collection. [StoreRaw] calls QueryAsync. Placeholders are {parameterName}. Register services.AddMauiLocalStoreDao<T>() when you want the DAO in DI.

[StoreDao("users", typeof(Person))]
public interface IPersonDao
{
    Task<string> InsertAsync(Person item, CancellationToken cancellationToken = default);
    Task<Person?> FindByIdAsync(string id, CancellationToken cancellationToken = default);

    [StoreRaw(
        Sql = "SELECT * FROM users WHERE Age >= {minAge}",
        Nql = "db.users.find({ age: { $gte: {minAge} } })")]
    Task<IReadOnlyList<Person>> FindAdultsAsync(int minAge, CancellationToken cancellationToken = default);
}

var dao = store.GetDao<IPersonDao>();
var adults = await dao.FindAdultsAsync(21);

What 1.1 does not do

  • Copy collections unless AutoMigrate (or MigrateAsync) and Map<T> are set
  • Promote a JSON-fallback folder to a later native DuckDB / Firebird / RocksDB / LevelDB file without that migrate path
  • Room-style schema migrations inside one engine
  • SQL or NQL on engines whose QueryLanguage is None
  • Sibling PackageReference to OfflineSync, JobQueue, or FileVault

Platforms and version

Version 1.1.0. Library and sample share the OS TFMs: net10.0-android, net10.0-ios, net10.0-maccatalyst, plus net10.0-windows10.0.19041.0 when built on Windows. The library also packs net10.0 for tests and shared hosts.

Plugin.Maui.LocalStore on nuget.org

Source on GitHub

Discussion

Comment on Plugin.Maui.LocalStore. The thread lives on this component's GitHub repository (nuvyntralabs/Plugin.Maui.LocalStore). Sign in with GitHub — Giscus uses Discussions, Utterances uses Issues.