Plugin.Maui.LocalStore 1.1.0

dotnet add package Plugin.Maui.LocalStore --version 1.1.0
                    
NuGet\Install-Package Plugin.Maui.LocalStore -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Plugin.Maui.LocalStore" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Plugin.Maui.LocalStore" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Plugin.Maui.LocalStore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Plugin.Maui.LocalStore --version 1.1.0
                    
#r "nuget: Plugin.Maui.LocalStore, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Plugin.Maui.LocalStore@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Plugin.Maui.LocalStore&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Plugin.Maui.LocalStore&version=1.1.0
                    
Install as a Cake Tool

Plugin.Maui.LocalStore

NuGet

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

1.1 opens every engine in the table below. Host CRUD stays the same. See Platforms for which databases actually run on Android, iOS, Windows, and Mac Catalyst.

This is not JobQueue or RetryQueue (durable jobs). It is not OfflineSync (sync + conflicts). [StoreDao] is a Room-style generator over this facade, not androidx.room.

Package: https://www.nuget.org/packages/Plugin.Maui.LocalStore

Local / Embedded databases

Database Type Default path MAUI Offline Relationships Best for Status
SQLite Relational app.db ⭐⭐⭐⭐⭐ General-purpose local DB Shipped
NuvexaDB Document NoSQL app.nvx ⭐⭐⭐⭐⭐ Limited Embedded .nvx documents Shipped
Realm Object DB app.realm ⭐⭐⭐⭐ Mobile / offline-first Shipped
LiteDB Document NoSQL app.litedb ⭐⭐⭐⭐ Limited Embedded NoSQL Shipped
DuckDB Analytical SQL app.duckdb ⭐⭐⭐ Analytics / OLAP Shipped (JSON fallback on mobile)
SQLCipher Encrypted SQLite app.db ⭐⭐⭐⭐ Secure local DB Shipped
Firebird Embedded Relational app.fdb ⭐⭐⭐ More advanced relational DB Shipped (JSON fallback without fbembed)
LMDB Key-value app.lmdb/ ⭐⭐⭐ Limited Very fast key-value storage Shipped
RocksDB Key-value app.rocksdb/ ⭐⭐⭐ No High-performance storage Shipped (JSON fallback on mobile)
LevelDB Key-value app.leveldb/ ⭐⭐⭐ No Simple KV storage Shipped (JSON fallback when native is missing)

NuvexaDB is Nuventra.NuvexaDB (Nuvyntra Labs). Every row above is reached through the same IStoreCollection<T> methods.

Platforms

LocalStore targets Android, iOS, Windows, and Mac Catalyst. You still 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.

Database Android iOS Windows Mac Catalyst
SQLite Yes Yes Yes Yes
SQLCipher Yes Yes Yes Yes
NuvexaDB Yes Yes Yes Yes
LiteDB Yes Yes Yes Yes
Realm Yes Yes Yes Yes
LMDB Yes Yes Yes JSON fallback
DuckDB JSON fallback JSON fallback Yes (win-x64, win-arm64) JSON fallback
Firebird JSON fallback JSON fallback Yes (Embedded NuGet) JSON fallback
RocksDB JSON fallback JSON fallback Yes (win-x64 only) JSON fallback
LevelDB JSON fallback JSON fallback JSON fallback JSON fallback

If you select a database on an unsupported platform

Selection does not change. Keep o.Backend = StoreBackend.DuckDb (or Firebird, RocksDB, LevelDB, or LMDB on Catalyst). LocalStore.Open / UseMauiLocalStore does not throw.

On that OS, LocalStore stores each row as a JSON file (app.duckdb.kv/, app.fdb.kv/, or files under app.lmdb/, app.rocksdb/, app.leveldb/). InsertAsync, FindByIdAsync, ReplaceAsync, DeleteByIdAsync, and FindAsync still work. Filters run in memory. EnsureIndexAsync does nothing.

This is not DuckDB / Firebird / RocksDB / LevelDB / LMDB. Switching later to a native file on a supported OS still needs AutoMigrate + Map<T> (same as any other engine switch). Use SQLite, SQLCipher, NuvexaDB, LiteDB, or Realm when you need the real engine on every MAUI platform.

Common methods

Portable CRUD stays on IStoreCollection<T>. Raw SQL / NQL is optional on ILocalStore.

Operation Method
Create InsertAsync / InsertManyAsync
Read FindByIdAsync
Update ReplaceAsync
Delete DeleteByIdAsync
Select FindAsync(StoreFilter, StoreQuery)
Raw SQL / NQL ILocalStore.QueryAsync<T> / ExecuteAsync
Generated DAO store.GetDao<IPersonDao>()
Index EnsureIndexAsync
Close DisposeAsync
ILocalStore store = LocalStore.Current;
var users = store.GetCollection<Person>("users");

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 (the sample Person.Summary is ignored).

public sealed class Person
{
    public string? Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public string Status { get; set; } = "active";
    public string? City { get; set; }
}

Scalar types: string, int, long, double, float, bool, DateTime.

dotnet add package Plugin.Maui.LocalStore

Host registration is UseMauiLocalStore. Non-MAUI hosts can call services.AddMauiLocalStore(...) or LocalStore.Open(...).

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

Option Default Used by
Backend Nuvexa All
Path see ResolvePath All (file or directory, per engine)
CreateIfMissing true All. false throws LocalStoreException if the file is missing
EncryptionKey none Nuvexa (required to open an encrypted .nvx), SQLCipher (required), LiteDB password, Realm, Firebird SYSDBA password. Ignored by SQLite, DuckDB, LMDB, RocksDB, LevelDB
CacheSizeMb 16 Nuvexa only
AutoMigrate false Copy Map<T> collections from another engine file when the destination is empty
MigrateFrom none Source engine. Required when more than one sibling file exists
MigrateFromPath none Source file. Empty uses the destination folder
MigrateFromEncryptionKey none Source key. Empty reuses EncryptionKey
DeleteSourceAfterMigrate false Remove the source file after a successful copy
Map<T>(name) none Registers a collection for migrate (required when AutoMigrate is true)

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.

The Create / Read / Update / Delete / Select samples in each engine section below are the same methods. Only StoreBackend and the file path change.

Engine NuGet references (library)

Backend Package
SQLite sqlite-net-base, SQLitePCLRaw.bundle_e_sqlite3
SQLCipher same mapping + SQLitePCLRaw.bundle_e_sqlcipher (do not also reference sqlite-net-sqlcipher)
NuvexaDB Nuventra.NuvexaDB 1.0.5
LiteDB LiteDB
Realm Realm
DuckDB DuckDB.NET.Data.Full (desktop natives)
Firebird FirebirdSql.Data.FirebirdClient, FirebirdDb.Embedded.V5.NativeAssets.Windows.All, FirebirdDb.Embedded.V5.NativeAssets.Linux.All
LMDB LightningDB
RocksDB RocksDB (desktop natives)
LevelDB LevelDB.Standard with ExcludeAssets=native;build;buildTransitive (Android cannot load those native assets)

SQLite

Relational. File: app.db. One table per collection. EncryptionKey is ignored — use SQLCipher when you need an encrypted SQLite file.

Register

using Plugin.Maui.LocalStore;

builder
    .UseMauiApp<App>()
    .UseMauiLocalStore(o =>
    {
        o.Backend = StoreBackend.Sqlite;
        o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.db");
        o.CreateIfMissing = true;
    });

var store = LocalStore.Current; // Backend == StoreBackend.Sqlite
var users = store.GetCollection<Person>("users");

Or without MAUI:

await using var store = LocalStore.Open(new LocalStoreOptions
{
    Backend = StoreBackend.Sqlite,
    Path = path
});

GetCollection creates the table on first write.

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada",
    Age = 36,
    Status = "active",
    City = "London"
});
// generated when Person.Id is null; written back onto the POCO

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);
if (ada is null)
{
    return;
}

Update

ReplaceAsync requires a non-empty Id. It updates the row; it does not insert. Missing id throws LocalStoreException.

ada.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);
// false when the id is not present

Select

FindAsync with no filter returns every row. StoreQuery.Limit of 0 means no limit. SQLite runs this as SQL on the table columns.

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));
Filter Meaning
StoreFilter.Eq("City", "London") equal
StoreFilter.Ne("Status", "retired") not equal
StoreFilter.Gte("Age", 21) greater than or equal
StoreFilter.Lt("Age", 30) less than
StoreFilter.And(...) all children
StoreFilter.Or(...) any child
await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

Close and delete the file

await store.DisposeAsync();
File.Delete(path);

NuvexaDB

Document NoSQL. File: app.nvx. One collection per name. Set EncryptionKey for AES-256-GCM. Opening an encrypted file without the key throws NuvexaEncryptionException (fail-closed). Store the key in SecureStorage — not in source.

Engine: Nuventra.NuvexaDBGitHub.

Register

using Plugin.Maui.LocalStore;

builder
    .UseMauiApp<App>()
    .UseMauiLocalStore(o =>
    {
        o.Backend = StoreBackend.Nuvexa;
        o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.nvx");
        o.EncryptionKey = key;
        o.CreateIfMissing = true;
        o.CacheSizeMb = 16;
    });

var store = LocalStore.Current; // Backend == StoreBackend.Nuvexa
var users = store.GetCollection<Person>("users");

Or without MAUI:

await using var store = LocalStore.Open(new LocalStoreOptions
{
    Backend = StoreBackend.Nuvexa,
    Path = path,
    EncryptionKey = key
});

GetCollection creates the collection on first write. Same Person type and same method names as every other engine.

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada",
    Age = 36,
    Status = "active",
    City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);
if (ada is null)
{
    return;
}

Update

ada.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

Same StoreFilter / StoreQuery as SQLite. Nuvexa maps POCO names to NQL paths (Ageage, Id_id).

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));
await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

Close and delete the file

await store.DisposeAsync();
File.Delete(path);
var wal = path + "-wal";
if (File.Exists(wal))
{
    File.Delete(wal);
}

Raw SQL / NQL

ILocalStore.QueryAsync<T> / ExecuteAsync are on the shared store. The dialect is store.QueryLanguage.

Engine QueryLanguage Command
SQLite, SQLCipher Sql SQL
DuckDB, Firebird Sql when native; None on the JSON fallback SQL when native
Nuvexa Nql NQL
LiteDB, Realm, LMDB, RocksDB, LevelDB None throws 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+.

Automatic migration

Each engine keeps its own file. On open, LocalStore can copy registered collections when the destination is empty.

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");
});

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.

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.

Source-generated DAOs

[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);

CRUD method names map to IStoreCollection<T>. [StoreRaw] calls QueryAsync. Register services.AddMauiLocalStoreDao<IPersonDao>() when you want the DAO in DI.


Realm

Object database. File: app.realm. Host POCOs stay plain classes. Rows are stored as JSON on a Realm object. Live Realm thread confinement is handled inside the adapter. Optional EncryptionKey encrypts the Realm file.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.Realm;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.realm");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

LiteDB

Embedded document NoSQL. File: app.litedb. Closest peer to Nuvexa on the common API: named collections, BSON-style documents, limited relationships. Optional EncryptionKey becomes the LiteDB password.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.LiteDb;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.litedb");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

DuckDB

Analytical SQL (OLAP). File: app.duckdb. Same CRUD; FindAsync maps to SQL. DuckDB.NET.Data.Full ships desktop natives only. On Android / iOS, LocalStore keeps the same IStoreCollection<T> API on a managed JSON folder (app.duckdb.kv).

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.DuckDb;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.duckdb");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

SQLCipher

Encrypted SQLite. File: app.db. Same relational mapping as SQLite. EncryptionKey is required. Do not reuse an unencrypted SQLite file as a SQLCipher path.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.SqlCipher;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app-cipher.db");
    o.EncryptionKey = key;
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

Do not reuse an unencrypted SQLite app.db as a SQLCipher path. Dispose, then open a different file.


Firebird Embedded

Advanced relational. File: app.fdb. Same table-per-collection mapping as SQLite. The host still calls IStoreCollection<T>. Desktop opening needs a Firebird 5 embedded tree (FIREBIRD pointing at the folder that contains lib/, plugins/, and bin/isql). Windows and Linux can use the FirebirdDb.Embedded.V5.NativeAssets.* packages. macOS has no NuGet native assets — use the official Firebird 5 package and set FIREBIRD / FIREBIRD_CLIENT. Android / iOS / Mac Catalyst have no fbembed package; LocalStore uses the managed JSON folder (app.fdb.kv) so CRUD still works. Optional EncryptionKey is the SYSDBA password on a real Firebird file (masterkey when omitted). Use AutoMigrate + Map<T> when switching engines.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.Firebird;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.fdb");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

LMDB

Key-value. Path: app.lmdb (directory). Each collection is a key prefix. The POCO is stored as JSON keyed by Id. FindAsync filters in memory. EnsureIndexAsync is a no-op. Relationships are limited.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.Lmdb;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.lmdb");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

RocksDB

Key-value. Path: app.rocksdb (directory). High-write LSM storage. Same Id → JSON mapping as LMDB. FindAsync scans the prefix and filters in memory. EnsureIndexAsync is a no-op. The RocksDB NuGet ships desktop natives only. On Android / iOS, LocalStore uses the same managed per-key JSON files as LevelDB.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.RocksDb;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.rocksdb");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

LevelDB

Key-value. Path: app.leveldb (directory). Same collection + Id contract as RocksDB. FindAsync filters in memory. EnsureIndexAsync is a no-op. If the native LevelDB library is missing for the RID (osx-arm64 is not in LevelDB.Standard; Android native assets are excluded because they are not PE), LocalStore uses a managed per-key JSON file store so the same API still works.

Register

builder.UseMauiLocalStore(o =>
{
    o.Backend = StoreBackend.LevelDb;
    o.Path = Path.Combine(FileSystem.AppDataDirectory, "app.leveldb");
    o.CreateIfMissing = true;
});

var users = LocalStore.Current.GetCollection<Person>("users");

Create

var id = await users.InsertAsync(new Person
{
    Name = "Ada", Age = 36, Status = "active", City = "London"
});

var ids = await users.InsertManyAsync(
[
    new Person { Name = "Grace", Age = 85, Status = "retired", City = "NewYork" },
    new Person { Name = "Cara", Age = 21, Status = "active", City = "Bengaluru" },
    new Person { Name = "Alan", Age = 42, Status = "active", City = "London" }
]);

Read

var ada = await users.FindByIdAsync(id);

Update

ada!.Name = "Ada Lovelace";
await users.ReplaceAsync(ada);

Delete

var removed = await users.DeleteByIdAsync(id);

Select

var all = await users.FindAsync();

var adults = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Limit = 20 });

var page2 = await users.FindAsync(
    StoreFilter.Gte("Age", 21),
    new StoreQuery { SortBy = "Name", Skip = 20, Limit = 20 });

var londonActive = await users.FindAsync(
    StoreFilter.And(
        StoreFilter.Eq("City", "London"),
        StoreFilter.Eq("Status", "active")),
    new StoreQuery { SortBy = "Age", SortDescending = true });

var youngOrNy = await users.FindAsync(
    StoreFilter.Or(
        StoreFilter.Lt("Age", 30),
        StoreFilter.Eq("City", "NewYork")));

var notRetired = await users.FindAsync(StoreFilter.Ne("Status", "retired"));

await users.EnsureIndexAsync("Age");
await users.EnsureIndexAsync("City", "Status");

Add another database later

A new engine implements ILocalStore / IStoreCollection<T> and a StoreBackend value. Host code stays on the common methods. Dispose, then Open with the new backend and a different path — data does not copy.

await LocalStore.Current.DisposeAsync();
LocalStore.Open(new LocalStoreOptions
{
    Backend = StoreBackend.Sqlite, // or Nuvexa, Realm, LiteDb, DuckDb, SqlCipher, Firebird, Lmdb, RocksDb, LevelDb
    Path = Path.Combine(FileSystem.AppDataDirectory, "app.db")
});

CreateIfMissing = false throws LocalStoreException when the file is missing.

What 1.0 does not do

  • Automatic migration between engines
  • Source-generated DAOs or raw SQL / NQL on the shared interface
  • Automatic promotion from the JSON fallback to a later native DuckDB / Firebird / RocksDB / LevelDB file
  • Sibling PackageReference to OfflineSync, JobQueue, or FileVault

Sample

samples/Plugin.Maui.LocalStore.Sample uses the same OS TFMs as the library: net10.0-android, net10.0-ios, net10.0-maccatalyst, and net10.0-windows10.0.19041.0 when the sample is built on Windows. MauiProgram does not call UseMauiLocalStore — the Backend picker calls LocalStore.Open so you can walk every engine. A host app that uses one engine should register it with UseMauiLocalStore.

Use insert / update / delete / find by Id, FindAsync presets (all, Age ≥ 21, London AND active, Age < 30 OR NewYork, custom), Seed, Reset file, Contract tour, and Test all engines. The 1.1 buttons run Migrate SQLite → Nuvexa, raw QueryAsync (SQL or NQL), and the generated IPersonDao. Test migrate + DAO asserts those two flows. DuckDB, Firebird, and RocksDB pass on device via the JSON fallback (CRUD only; QueryLanguage is None). SQLCipher uses app-cipher.db so it does not share the SQLite app.db.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-android36.0 is compatible.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst was computed.  net10.0-maccatalyst26.0 is compatible.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net10.0-windows10.0.19041 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 17 9/15/2026
1.0.1 45 9/14/2026
1.0.0 38 9/14/2026

1.1.0: Automatic engine migration (AutoMigrate + Map<T>). Raw SQL/NQL on ILocalStore. Source-generated [StoreDao] implementations.