# Shiny Stores

> Generate and configure Shiny Stores for .NET - cross-platform key/value stores with source-generated property binding for mobile, desktop, and Blazor WebAssembly

- Skill: `shinyorg/shiny-stores` (Agent Skill)
- Install (CLI): `npx skillmds@latest add shinyorg/shiny-stores`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shinyorg/shiny-stores/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: shinyorg (https://skillmd.com/u/shinyorg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shinyorg/shiny-stores

---


# Shiny Stores Skill

You are an expert in Shiny Extensions Stores, a .NET library providing cross-platform key/value store abstraction with source-generated property binding.

## When to Use This Skill

Invoke this skill when the user wants to:
- Use cross-platform key/value stores (settings, secure storage, memory)
- Persist properties to a backing store using the `[Bind]` source generator
- Use key/value stores in Blazor WebAssembly (localStorage)
- Create custom key/value store implementations

## Library Overview

**Documentation**: https://shinylib.net/extensions/stores/
**Repository**: https://github.com/shinyorg/Shiny.Extensions
**Packages**: `Shiny.Extensions.Stores`, `Shiny.Extensions.Stores.Web`

## Built-in Store Keys

Stores are registered as **keyed** singletons in DI using `StoreKeys` constants:

| Key | Platform | Implementation |
|-----|----------|---------------|
| `StoreKeys.Default` ("settings") | Android | SharedPreferences |
| `StoreKeys.Default` ("settings") | iOS / Mac Catalyst / macOS (`net10.0-macos`) | NSUserDefaults |
| `StoreKeys.Default` ("settings") | Windows (packaged) | ApplicationData.LocalSettings |
| `StoreKeys.Default` ("settings") | Linux / other desktop / unpackaged Windows | `FileKeyValueStore` (JSON file) |
| `StoreKeys.Default` ("settings") | Blazor | localStorage |
| `StoreKeys.Secure` ("secure") | Android | EncryptedSharedPreferences |
| `StoreKeys.Secure` ("secure") | iOS / Mac Catalyst / macOS (`net10.0-macos`) | Keychain |
| `StoreKeys.Secure` ("secure") | Windows | DPAPI (packaged: ApplicationData; unpackaged: over file) |
| `StoreKeys.Secure` ("secure") | Linux / other desktop | `FileKeyValueStore` (JSON file, **not encrypted**) |

`net10.0-macos` (plain macOS desktop apps) uses Foundation NSUserDefaults + Security.framework
Keychain — the same code as iOS/Mac Catalyst — so it gets **real** secure storage, not the plaintext
file fallback. Any target that resolves the base `net10.0` asset (Linux, unpackaged Windows, other
desktop) uses `FileKeyValueStore`, which persists to `{LocalApplicationData}/{EntryAssemblyName}` — so
settings survive restarts instead of living only in memory. Override the folder via
`Shiny.Stores.FileStoreDirectory` **before first access**. On these file fallbacks `Secure` is a plain
JSON file and is **not encrypted** (unpackaged Windows keeps DPAPI over the file); do not put
genuinely sensitive secrets there.

On Apple platforms (iOS, Mac Catalyst, `net10.0-macos`) the default store is scoped to the app's
**own** persistent domain. `NSUserDefaults.StandardUserDefaults` is a *search list* — it resolves a
key through the argument domain, the app's domain, the global domain and the registration domain —
so `Contains`/`Get`/`Remove` ask `PersistentDomainForName(bundleId)` rather than the merged list, and
`Clear` drops that domain. Without this, an ordinary key name (`AutoRecord`, `UseMetric`, `Enabled`)
that any linked framework registered a default for reads back as **present** having never been
written, so `store.Get(key, defaultValue)` and `[Bind(Default = ...)]` silently return the foreign
value instead of the declared default. Keys the app itself wrote are unaffected — the app's own
domain outranks the global and registration domains.

## Setup

```csharp
// Mobile / desktop — platform-native stores
services.AddShinyStores();

// Blazor WebAssembly — localStorage (still needs UseShinyStores after Build,
// because IJSRuntime is only available post-build)
services.AddShinyWebAssemblyStores();
```

On mobile/desktop you **do not need** a post-build `UseShinyStores()` call.
`Shiny.Stores.Default` / `Shiny.Stores.Secure` are self-bootstrapping: on first
access they lazily create the platform-native store (SharedPreferences /
NSUserDefaults / Keychain / DPAPI) or a persistent `FileKeyValueStore` on desktop
targets that resolve the base `net10.0` asset. `AddShinyStores()`
just registers those same instances into DI so keyed `IKeyValueStore` injections
share them.

For Blazor (where the store needs `IJSRuntime` from the built provider) call
`host.Services.UseShinyStores()` after `host.Build()` to snapshot the keyed
`IKeyValueStore` registrations into the static accessor.

## Static `Shiny.Stores` Accessor

The simplest way to read/write — self-bootstraps on first access. No
initialization required for mobile/desktop.

```csharp
Shiny.Stores.Default.Set("theme", "dark");
var theme = Shiny.Stores.Default.Get<string>("theme");

Shiny.Stores.Secure.Set("token", "abc123");

// Arbitrary keyed stores (must be registered with Stores.Register or via DI + UseShinyStores)
Shiny.Stores.Keyed("my-store").Set("k", "v");
```

### Overrides / Tests / Custom Keys

```csharp
// Swap any key for a test double or custom backend
Shiny.Stores.Register(StoreKeys.Default, new MemoryKeyValueStore());
Shiny.Stores.Register("redis", new RedisKeyValueStore(...));

// Or snapshot keyed IKeyValueStore registrations from a built provider:
serviceProvider.UseShinyStores();
// Equivalent low-level call:
Shiny.Stores.Initialize(serviceProvider);

// Reset between tests:
Shiny.Stores.Reset();
```

## DI-Style Access

```csharp
public class SettingsService(
    [FromKeyedServices(StoreKeys.Default)] IKeyValueStore settings,
    [FromKeyedServices(StoreKeys.Secure)] IKeyValueStore secure
)
{
    public void SaveTheme(string theme) => settings.Set("theme", theme);
    public string GetTheme() => settings.Get<string>("theme") ?? "light";
}
```

`AddShinyStores()` also registers the default store **unkeyed**, so a plain
`IKeyValueStore` parameter resolves to the same instance as
`[FromKeyedServices(StoreKeys.Default)]`.

### Third-party containers

Container adapters that predate .NET 8 keyed services — Prism's DryIoc container is the
common one, it still sits on DryIoc 5.x — silently **ignore** `[FromKeyedServices]` and
resolve the plain service type instead. The unkeyed registration above is what keeps the
default store working there. `StoreKeys.Secure` has no such fallback (registering two
different stores under one service type would be ambiguous), so when generating code for
an app on a non-Microsoft container, reach for the static accessor instead:

```csharp
public class SettingsService
{
    readonly IKeyValueStore secure = Shiny.Stores.Secure;   // or Shiny.Stores.Keyed("my-store")
}
```

A DryIoc `Error.UnableToFindCtorWithAllResolvableArgs` naming a type that injects a keyed
store is this problem — DryIoc's `ConstructorWithResolvableArguments` rule reports the
outermost type, not the dependency that actually failed.

## Source-Generated `[Bind]` Properties

The DI source generator (from `Shiny.Extensions.DependencyInjection`) recognizes `[Bind]` on partial properties and emits getter/setter bodies that round-trip through the static `Shiny.Stores` accessor.

```csharp
using Shiny;

[Singleton]
public partial class AppSettings
{
    [Bind]                                   // default store
    public partial string Theme { get; set; }

    [Bind("secure")]                         // secure store
    public partial string Token { get; set; }

    [Bind(Key = "ui_density")]               // override storage key
    public partial int Density { get; set; }
}
```

No `INotifyPropertyChanged`, no runtime reflection. Generated property bodies call `Shiny.Stores.Default/Secure/Keyed(...).Get<T>(...)` and `.Set(...)`.

## Store Extension Methods

```csharp
store.Get<T>(key, defaultValue);        // Get with default
store.GetRequired<T>(key);              // Throws if not found
store.SetOrRemove(key, value);          // Removes if value is null
store.SetDefault<T>(key, value);        // Only sets if key doesn't exist
store.IncrementValue(key);              // Thread-safe integer increment
```

## Code Generation Instructions

- Use `AddShinyStores()` for mobile/desktop, `AddShinyWebAssemblyStores()` for Blazor
- For persistent settings, prefer `[Singleton]` + `[Bind]` partial properties over manual `Set`/`Get` calls
- The class with `[Bind]` properties must be `partial`; properties must also be `partial`
- For sensitive data (tokens, credentials), pass `"secure"` to `[Bind("secure")]`
- Use `Shiny.Stores.Default`/`Secure`/`Keyed(...)` for direct ad-hoc access

## Best Practices

1. **Use `[Bind]` for settings classes** — eliminates boilerplate, no INPC needed, AOT-clean
2. **Target the secure store** — always use `[Bind("secure")]` for sensitive values
3. **Mobile/desktop needs no post-build call** — `Shiny.Stores` self-bootstraps on first access. `UseShinyStores()` is only needed for Blazor (because `IJSRuntime` requires the built provider) or when you've registered custom keyed `IKeyValueStore`s in DI that you want snapshotted into the static accessor
4. **Use `Stores.Register` in tests** — pair with `Stores.Reset()` between tests to swap in `MemoryKeyValueStore` or any custom double

