Skip to main content

shiny-firestore-mobile

Generate code using Shiny.DocumentDb.Firestore.Mobile, an on-device (native) Firebase Firestore provider for Shiny.DocumentDb on iOS and Android — offline-first persistence, real-time snapshot listeners, LINQ queries, and managed Firebase Auth identity.

Aller à l'installation

Informations de source

Dépôt
shinyorg/skills
Dernière activité de la source
15 août 2026 à 17:13
Langue détectée de SKILL.md
anglais
Étoiles
4
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
shiny-firestore-mobile
description
Generate code using Shiny.DocumentDb.Firestore.Mobile, an on-device (native) Firebase Firestore provider for Shiny.DocumentDb on iOS and Android — offline-first persistence, real-time snapshot listeners, LINQ queries, and managed Firebase Auth identity.
auto_invoke
true
triggers
["firestore mobile","mobile firestore","on-device firestore","native firestore","offline firestore","Shiny.DocumentDb.Firestore.Mobile","MobileFirestoreDocumentStore","MobileFirestoreOptions","AddMobileFirestoreDocumentStore","ConfigureDocument","ToCollection","AddFirebaseIdentity","IFirebaseIdentity","FirebaseRestIdentity","FirebaseIdentityOptions","FirebaseUser","PersistenceEnabled","EmulatorHost","firestore emulator","firestore offline","firestore realtime","firestore snapshot listener","firebase auth maui","firebase anonymous sign in"]
# Shiny Mobile Firestore Provider ## Overview `Shiny.DocumentDb.Firestore.Mobile` is an **on-device** Firebase Firestore provider for [Shiny.DocumentDb](https://shinylib.net). It binds the **native** Firebase Firestore SDK (via a first-party Shiny binding, *not* a third-party one) and adapts it to the `IDocumentStore` contract. The native SDK owns the hard parts — local cache, offline write queue, real-time listeners, backoff, conflict handling. This provider is a thin typed adapter over it. ### This is NOT `Shiny.DocumentDb.Firestore` Do not confuse the two packages. Pick by **where the code runs**: | | `Shiny.DocumentDb.Firestore` | `Shiny.DocumentDb.Firestore.Mobile` (this skill) | |---|---|---| | SDK | `Google.Cloud.Firestore` (admin/gRPC) | Native Firebase Firestore SDK (binding) | | Runs where | Server / backend host | On the client device | | Auth | Service account (ADC) | Firebase Auth, per end-user | | Security rules | Bypassed (admin) | Enforced (`request.auth.uid`) | | Offline | None | Native persistent cache — offline by default | | Registration | `AddFirestoreDocumentStore(...)` | `AddMobileFirestoreDocumentStore(...)` | **Never embed a service-account key in a shipped app** — that is what the server package is for. ## Platform support — read this first | TFM | Behavior | |---|---| | `net10.0-android` | Real adapter over the native SDK. Verified end-to-end against the Firestore emulator. | | `net10.0-ios` | Real adapter over the native SDK (slim Swift binding). Verified end-to-end against the Firestore emulator. | | `net10.0` | **Throw-stub.** Every operation throws `PlatformNotSupportedException`. Exists so the surface is unit-testable without a device. | Both mobile heads are at **feature parity** — the same operations work and the same ones throw, and both pass the same 9-case emulator harness (CRUD, real-time, query/order/count/delete, identity). The API below applies identically to Android and iOS. Anything other than these two TFMs throws `PlatformNotSupportedException`, so multi-targeted app code must guard the mobile paths (`#if ANDROID || IOS`) or avoid resolving the store elsewhere. ## Installation ```bash dotnet add package Shiny.DocumentDb.Firestore.Mobile ``` The Android head pulls AndroidX / Guava / DataStore from nuget.org. If the consuming app inherits a user-level private NuGet source, add a `nuget.config` that clears sources back to nuget.org. ## Setup ### 1. Initialize FirebaseApp The store resolves Firebase in its constructor and **throws `InvalidOperationException` if Firebase is not initialized**. Bundle the platform config file — `google-services.json` (Android) / `GoogleService-Info.plist` (iOS) — for auto-init, or initialize explicitly before the store is resolved. On iOS, setting `ProjectId` + `AppId` on the options lets the provider configure Firebase itself; a config file already loaded by the app always wins. ### 2. Register the store ```csharp using Shiny.DocumentDb; builder.Services.AddMobileFirestoreDocumentStore(o => { o.ProjectId = "my-project"; // optional when google-services.json is bundled o.PersistenceEnabled = true; // default — offline cache on // per-type mapping always goes in a ConfigureDocument<T> block o.ConfigureDocument<Play>(cfg => { cfg.ToCollection("plays"); // default collection = type name cfg.MapIdProperty(x => x.Id); // default: a property named Id cfg.AddQueryFilter(p => p.Version >= 1); }); }); ``` > **Requires `Shiny.DocumentDb` 13.x.** The flat per-type methods (`o.MapTypeToCollection<T>`, > `o.MapIdProperty<T>`, `o.AddQueryFilter<T>`, `o.MapVersionProperty<T>`, `o.OnBeforeWrite<T>`, > `o.OnAfterWrite<T>`) were **removed** in 3.0.0 — never generate them. They all live on the > `ConfigureDocument<T>` builder now. Store-level members (`MapIdType<TId>`, `AddInterceptor`, > `AddBulkInterceptor`) are unchanged. This registers one singleton exposed as four contracts — all the **same instance**: - `IDocumentStore` — CRUD + `Query<T>()` - `IDocumentMaintenance` - `IObservableDocumentStore` — `NotifyOnChange<T>()` - `IChangeFeedDocumentStore` — `SubscribeChanges<T>()` ### 3. Register identity (optional) ```csharp builder.Services.AddFirebaseIdentity(o => o.ApiKey = "your-web-api-key"); ``` ## Document requirements - Each document type maps to its own collection — the type name by default, overridable with `cfg.ToCollection(name)` (or the provider-agnostic `cfg.Table = name`). - The document **id is the Firestore document id**. It comes from a property named `Id` by default (`cfg.MapIdProperty(...)` to override), matched **case-insensitively** against the serialized JSON. - The id must be present and non-empty on every write — an empty id throws `InvalidOperationException`. This provider does **not** generate ids for you. - Documents are stored as native Firestore field maps converted from `System.Text.Json`. **Field names are the JSON property names** — if you set a `PropertyNamingPolicy`, queries resolve against the converted name automatically, but hand-written field strings must match the JSON name. ```csharp class Play { public string Id { get; set; } = null!; public string Name { get; set; } = null!; public int Version { get; set; } } ``` ## CRUD ```csharp var store = sp.GetRequiredService<IDocumentStore>(); await store.Insert(new Play { Id = "p1", Name = "Slant Left", Version = 1 }); var play = await store.Get<Play>("p1"); // null when absent await store.Upsert(new Play { Id = "p1", Name = "Slant Right", Version = 2 }); await store.Remove<Play>("p1"); // always returns true — Firestore delete is idempotent var cleared = await store.Clear<Play>(); // deletes the collection doc-by-doc, returns the count ``` **`Insert` is an upsert.** `Insert`, `Update`, and `Upsert` all map to native Firestore `set()`, which overwrites. There is no insert-if-absent semantic yet — `Insert` on an existing id silently replaces it. If a user needs true insert-if-absent, `Get` first (racy) or wait for the transactional milestone. ## Querying `Query<T>()` returns an `IDocumentQuery<T>` backed by a native Firestore query. Filters, ordering, and limit push down to the native query; **aggregates and pagination offset are applied client-side over materialized results**. ```csharp var plays = await store .Query<Play>() .Where(p => p.Version >= 2) .OrderBy(p => p.Version) .ToList(); var count = await store.Query<Play>().Count(); var any = await store.Query<Play>().Where(p => p.Name == "Alpha").Any(); var n = await store.Query<Play>().Where(p => p.Version >= 2).ExecuteDelete(); await store.Query<Play>().ExecuteUpdate(p => p.Name, "Renamed"); ``` Supported: `Where`, `OrderBy`, `OrderByDescending`, `Paginate`, `ToList`, `ToAsyncEnumerable`, `Count`, `Any`, `ExecuteDelete`, `ExecuteUpdate`, `Max`, `Min`, `Sum`, `Average`, `NotifyOnChange`, `IgnoreQueryFilters`. Translated predicate operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, and `ICollection.Contains`. Anything else throws. The compared-against value may be a literal, a captured local, a field/property read, a method call, or an inline array (`new[] { "a", "b" }.Contains(p.Name)`). It is evaluated by walking the expression, not by compiling it, so generated predicates behave identically on a full-AOT build. ### Query gotchas - **Firestore rule: with an inequality filter, the first `OrderBy` must be on the inequality field.** `.Where(p => p.Version >= 2).OrderBy(p => p.Version)` is valid; ordering by a different field first fails at the native SDK. - `Count()` on a query **materializes every document** — it is not a native aggregate. `Count<T>()` on the store does the same. Avoid on large collections. - `Paginate(offset, take)` issues a native `Limit(offset + take)` and skips `offset` client-side — Firestore has no offset. Deep pagination reads everything up to the offset. - `Max`/`Min`/`Sum`/`Average` materialize and compute in managed code. - `Select` projection throws `NotSupportedException` — `ToList` and project client-side. - `IgnoreQueryFilters(params string[])` ignores the names and drops **all** filters (all-or-nothing today), and it **restarts the query from the collection** — call it before any `Where`, or that `Where` is lost. - **The builder returns a copy** (as of 2.0.0, matching every other provider). Always assign the result — `q.Where(…);` on its own line is a no-op. Branching is safe: `var b = a.Where(…)` leaves `a` alone. - Grouping, cursor paging, vector, and full-text throw. ## Real-time Both are backed by native snapshot listeners. **The initial snapshot is skipped** — you receive changes from the point of subscription, not the current contents. Read with `Get`/`ToList` first if you need a baseline. ```csharp // Callback style — dispose the handle to detach the listener var sub = await changeFeedStore.SubscribeChanges<Play>((change, ct) => { Console.WriteLine($"{change.ChangeType}: {change.Id} {change.Document?.Name}"); return Task.CompletedTask; }); await sub.DisposeAsync(); // Stream style — the listener detaches when enumeration ends await foreach (var change in observableStore.NotifyOnChange<Play>(ct)) Console.WriteLine(change.ChangeType); // Scoped to a filtered query await foreach (var change in store.Query<Play>().Where(p => p.Version > 1).NotifyOnChange(ct)) Console.WriteLine(change.Id); ``` `change.ChangeType` is `DocumentChangeType.Inserted` / `Modified` / `Removed`. ## Offline `PersistenceEnabled` (default `true`) turns on the native persistent cache — reads are cache-first, writes queue locally and drain automatically on reconnect. This is the entire point of the mobile provider; leave it on unless a test needs a clean slate. Because writes queue offline, an `await store.Insert(...)` that completes does **not** guarantee the server has the write — only that the native SDK accepted it. ## Identity `IFirebaseIdentity` is a **managed** implementation over the Firebase Auth REST API (no native binding). ```csharp var identity = sp.GetRequiredService<IFirebaseIdentity>(); var user = await identity.SignInAnonymouslyAsync(); await identity.SignUpWithEmailPasswordAsync("a@b.com", "Passw0rd!"); await identity.SignInWithEmailPasswordAsync("a@b.com", "Passw0rd!"); var token = await identity.GetIdTokenAsync(); // auto-refreshes within 1 min of expiry var uid = identity.CurrentUserId; identity.AuthStateChanged += (_, u) => { /* u is null on sign-out */ }; identity.SignOut(); ``` `FirebaseUser` is a record: `Uid`, `IdToken`, `RefreshToken`, `ExpiresAt`, `IsAnonymous`, `Email`. ### Critical limitation — the token does not reach the native SDK The identity service obtains and refreshes the user's token **in managed code only**. It is *not* wired into the native Firestore SDK's request auth, so **Firestore security rules do not see `request.auth.uid` for native reads and writes yet**. That integration needs a native `signInWithCustomToken` and is deferred with the native auth binding. Until then, **scope per-user data by collection path**: ```csharp o.ConfigureDocument<Play>(cfg => cfg.ToCollection($"users/{uid}/plays")); ``` Do not tell users their rules are enforced per-user today, and do not generate rules that rely on `request.auth.uid` for this provider's traffic without flagging this gap. ## Options reference — `MobileFirestoreOptions` | Member | Notes | |---|---| | `ProjectId` / `AppId` / `ApiKey` | Optional when a platform config file is bundled; takes precedence for explicit init | | `PersistenceEnabled` | Default `true` — native offline cache | | `EmulatorHost` | `host:port`, points the native SDK at the emulator | | `TypeNameResolution` | Default `ShortName` | | `JsonSerializerOptions` | Drives field names and (de)serialization | | `UseReflectionFallback` | Default `true`; set `false` for iOS full-AOT — see [Trimming and AOT](#trimming-and-aot) | | `Logging` | `Action<string>` diagnostic callback | | `MapIdType<TId>(...)` | Custom id CLR types beyond Guid/int/long/string | | `AddInterceptor` / `AddBulkInterceptor` | **Registered but not invoked today** — see below | | `ConfigureDocument<T>(cfg => …)` | Everything per-type — the table below | ### Per-type — inside `o.ConfigureDocument<T>(cfg => …)` | Member | Notes | |---|---| | `cfg.ToCollection(name)` | Override the collection (default: type name). `cfg.Table = name` is the same thing | | `cfg.MapIdProperty(x => x.MyId)` | Override the id property (default: `Id`) | | `cfg.AddQueryFilter(predicate)` | Global filter, auto-applied to every query; named overload available | | `cfg.MapVersionProperty(...)` | **Registered but not enforced today** — see below | | `cfg.OnBeforeWrite(...)` / `cfg.OnAfterWrite(...)` | **Registered but not invoked today** — see below | The builder is provider-agnostic, so it also offers `MapTemporal`, `MapBlob`, `MapSpatialProperty`, `MapVectorProperty`, `MapFullTextProperty` and `MapComputedProperty`. **This provider supports none of them** — mapping one throws `DocumentConfigurationException` when the store is built, listing every problem at once. Never generate them for a mobile Firestore store. ## Trimming and AOT The package is `IsAotCompatible` and warning-free. Document (de)serialization is the only reflection seam, and only when the caller omits a `JsonTypeInfo<T>`. **When generating code for a MAUI app that publishes trimmed or full-AOT, prefer the `JsonTypeInfo<T>` overloads:** ```csharp [JsonSerializable(typeof(Play))] public partial class AppJsonContext : JsonSerializerContext;
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub