Offline Sync
Shiny.DocumentDb.AppDataSync makes an IDocumentStore the local cache of an offline-first app that
bidirectionally syncs to an HTTP backend, by gluing it to
Shiny.Data.Sync. You write to the document store exactly as you
do today; the glue:
- Auto-queues every local
Insert/Update/Upsert/Removeon a sync-registered type into the Shiny.Data.Sync outbox — reliable, background-capable, surviving app kill on the platforms Shiny.Data.Sync supports. - Auto-applies every change pulled from the server back into the store (Create/Update →
Upsert, Delete →Remove) — without you hand-writing anIDataSyncDelegate.
dotnet add package Shiny.DocumentDb.AppDataSyncClient-tier providers: SQLite, LiteDB, IndexedDB (Blazor WASM) — the on-device stores that match Shiny.Data.Sync’s platform model. The server providers (Cosmos/Mongo/relational) are the sync backend, not the local cache.
builder.Services .AddDocumentStore(o => o.UseSqlite("app.db").MapTypeToTable<TodoItem>()) .AddDataSync<MyDataSyncDelegate>(opts => opts.RegisterEndpoint<TodoItem>("https://api.example.com/todos")) .SyncDocumentStore(sync => { sync.Sync<TodoItem>(); // wire the DocumentDb type to its registered sync endpoint });
// Application code — no Queue, no delegate. Local write -> outbox; server change -> store.await store.Upsert(new TodoItem { Id = id, Title = "Buy milk", Completed = false });Synced types implement Shiny.Data.Sync.ISyncEntity — its string Identifier is the sync key (set it
from your Id, e.g. Identifier => Id.ToString()). Shiny.Data.Sync’s RegisterEndpoint<T> and Queue<T>
constrain T : ISyncEntity, so this is required; the glue bridges that Identifier to the store’s Id
configuration internally.
public class TodoItem : ISyncEntity{ public Guid Id { get; set; } public string Identifier => this.Id.ToString(); // the sync key public string Title { get; set; } = ""; public bool Completed { get; set; }}How it works
Section titled “How it works”The glue contributes two stateless moving parts; it stores nothing of its own (the durable outbox is entirely Shiny.Data.Sync’s):
- Outbound — an
IDocumentInterceptorwhoseAfterWritemaps the operation to aSyncVerband callsIDataSyncManager.Queue(...). It runs inside the write transaction and then hands off; the queue it feeds is Shiny.Data.Sync’s. - Inbound — a supplied
IDataSyncDelegatewhoseOnReceivedapplies the pulled item back into the store through aUnitOfWorkcommitted withSaveChanges(suppressInterceptors: true). Because that suppresses interceptors, the apply does not echo back to the server (the loop guard), runs no validation/side-effects on mirrored data, and applies the whole pulled page atomically.
Inbound applies go through Upsert/Remove, which raise IObservableDocumentStore.NotifyOnChange<T> —
so a MAUI/Blazor view bound to the store updates automatically when a server change arrives.
Rules worth knowing
Section titled “Rules worth knowing”Set-based writes on synced types are rejected
Section titled “Set-based writes on synced types are rejected”ExecuteUpdate, ExecuteDelete, and Clear<T>() never materialize the affected documents, so they
can’t be enqueued per row. On a synced type they throw SyncBulkWriteNotSupportedException rather than
silently skipping the outbox. For a local-only whole-store wipe (sign-out / reset), use
IDocumentMaintenance.ClearAll — it fires no interceptor, so it commits locally without enqueuing.
Batch writes are fine: BatchInsert / BatchUpsert / BatchUpdate / BatchRemove on a synced type each
enqueue per item (registering the forwarder forces the per-document path).
One shared serializer
Section titled “One shared serializer”DocumentDb and Shiny.Data.Sync must serialize synced types through the same JsonSerializerOptions /
source-gen context — that single context defines the wire format the backend sees and guarantees an
inbound payload round-trips back through Upsert. SyncDocumentStore validates this at startup and
throws if they diverge.
Composes with validation
Section titled “Composes with validation”If Shiny.DocumentDb.JsonSchema is also registered, validation runs on
BeforeWrite and the forwarder enqueues on AfterWrite — so an invalid document throws and rolls back
before it can ever reach the outbox. No coordination required.