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
// 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.
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
// 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
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:
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.
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
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 manualSet/Getcalls - The class with
[Bind]properties must bepartial; properties must also bepartial - For sensitive data (tokens, credentials), pass
"secure"to[Bind("secure")] - Use
Shiny.Stores.Default/Secure/Keyed(...)for direct ad-hoc access
Best Practices
- Use
[Bind]for settings classes — eliminates boilerplate, no INPC needed, AOT-clean - Target the secure store — always use
[Bind("secure")]for sensitive values - Mobile/desktop needs no post-build call —
Shiny.Storesself-bootstraps on first access.UseShinyStores()is only needed for Blazor (becauseIJSRuntimerequires the built provider) or when you've registered custom keyedIKeyValueStores in DI that you want snapshotted into the static accessor - Use
Stores.Registerin tests — pair withStores.Reset()between tests to swap inMemoryKeyValueStoreor any custom double