| name | shiny-stores |
| description | Generate and configure Shiny Stores for .NET - cross-platform key/value stores with source-generated property binding for mobile, desktop, and Blazor WebAssembly |
| auto_invoke | true |
| triggers | ["IKeyValueStore","AddShinyStores","AddShinyWebAssemblyStores","StoreKeys","Shiny.Stores","BindAttribute","Shiny.Extensions.Stores","Shiny.Extensions.Stores.Web"] |
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
services.AddShinyStores();
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");
Shiny.Stores.Keyed("my-store").Set("k", "v");
Overrides / Tests / Custom Keys
Shiny.Stores.Register(StoreKeys.Default, new MemoryKeyValueStore());
Shiny.Stores.Register("redis", new RedisKeyValueStore(...));
serviceProvider.UseShinyStores();
Shiny.Stores.Initialize(serviceProvider);
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;
}
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]
public partial string Theme { get; set; }
[Bind("secure")]
public partial string Token { get; set; }
[Bind(Key = "ui_density")]
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);
store.GetRequired<T>(key);
store.SetOrRemove(key, value);
store.SetDefault<T>(key, value);
store.IncrementValue(key);
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
- 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.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 IKeyValueStores in DI that you want snapshotted into the static accessor
- Use
Stores.Register in tests — pair with Stores.Reset() between tests to swap in MemoryKeyValueStore or any custom double