# Shiny Extensions
## Dependency Injection Extensions
* Source generate all attributed classes to a single add file - saves you the boilerplate
* **Factory-form emission** - generated registrations expand the constructor at compile time (no reflection, AOT-clean) so resolve chains like `OnResolved` compose naturally
* `ActivatorUtilities`-style constructor selection (`[ActivatorUtilitiesConstructor]` and `[FromKeyedServices]` honored)
* Optional/nullable constructor parameters are respected - resolved via `GetService` (no throw when unregistered) with fallback to the declared default value
* Multiple interfaces via explicit forwarders (no reflection)
* Supports open generics and keyed services
* `OnResolved<T>(hook)` chain extension for one-shot post-construction hooks
### The Results
THIS:
```csharp
using Microsoft.Extensions.DependencyInjection;
using Shiny;
// given the following code from a user
namespace Sample
{
public interface IStandardInterface;
public interface IStandardInterface2;
[Service(ServiceLifetime.Singleton)]
public class ImplementationOnly;
[Service(ServiceLifetime.Transient, "ImplOnly")]
public class KeyedImplementationOnly;
[Service(ServiceLifetime.Singleton)]
public class StandardImplementation : IStandardInterface;
[Service(ServiceLifetime.Scoped, "Standard")]
public class KeyedStandardImplementation : IStandardInterface;
[Service(ServiceLifetime.Singleton)]
public class MultipleImplementation : IStandardInterface, IStandardInterface2;
[Service(ServiceLifetime.Scoped)]
public class ScopedMultipleImplementation : IStandardInterface, IStandardInterface2;
[Service(ServiceLifetime.Scoped, "KeyedGeneric")]
public class TestGeneric<T1, T2>
{
public T1 Value1 { get; set; }
public T2 Value2 { get; set; }
}
}
```
GENERATES THIS:
```csharp
// <auto-generated />
using global::Microsoft.Extensions.DependencyInjection;
using global::Shiny;
namespace Sample
{
public static class __GeneratedRegistrations
{
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddGeneratedServices(
this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
)
{
services.AddSingleton<global::Sample.ImplementationOnly>();
services.AddKeyedTransient<global::Sample.KeyedImplementationOnly>("ImplOnly");
services.AddSingleton<global::Sample.IStandardInterface, global::Sample.StandardImplementation>();
services.AddKeyedScoped<global::Sample.IStandardInterface, global::Sample.KeyedStandardImplementation>("Standard");
services.AddSingletonAsImplementedInterfaces<global::Sample.MultipleImplementation>();
services.AddScopedAsImplementedInterfaces<global::Sample.ScopedMultipleImplementation>();
services.AddKeyedScoped(typeof(global::Sample.TestGeneric<,>), "KeyedGeneric");
return services;
}
}
}
```
### Setup
1. Install the NuGet package `Shiny.Extensions.DependencyInjection`
2. Add the following using directive:
```csharp
// during your app startup - use your service collection
builder.Services.AddGeneratedServices();
```
3. Add the `[Service(ServiceLifetime.Singleton, "optional key")]` attribute to your classes and specify the lifetime and optional key
## Serialization
* Centralized `ISerializer` backed by source-generated `JsonSerializerContext`s, no reflection, AOT-clean
* `Shiny.Json` static accessor — self-bootstrapping, sibling to `Shiny.Stores` for mobile cold-start
* Primitives and common BCL value types (`string`, `bool`, numerics, `decimal`, `char`, `Guid`, `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`, `TimeSpan`, `Uri`, `byte[]` and their nullable forms) work out of the box — no context needed. Registered contexts still take precedence
* `[ShinyJsonContext]` on any user-declared `JsonSerializerContext` partial → source generator emits a `[ModuleInitializer]` that auto-registers it. No `services.AddJsonContext(...)` boilerplate needed
* `[ShinyJsonInclude]` on a type (or `[assembly: ShinyJsonInclude(typeof(T))]`) → AOT-safe collection wrappers for `List<T>`, `T[]`, `IEnumerable<T>`, `IReadOnlyList<T>`, `IList<T>`, `ICollection<T>`, `IAsyncEnumerable<T>` — solves the "inline `[JsonConverter]` works but `List<T>` fails" trap
* Multiple contributing libraries chain cleanly via `TypeInfoResolverChain`. Element types from one context compose with collection wrappers from another
* `services.AddJsonSerialization()` / `AddJsonContext(...)` / `ConfigureJsonSerializer(...)` for DI-side wiring; `Shiny.Json.CreateTestScope()` for test isolation
### Setup
1. Install the NuGet package `Shiny.Extensions.Serialization`
2. Write a normal STJ source-generator context and decorate it with `[Shiny.ShinyJsonContext]`:
```csharp
using System.Text.Json.Serialization;
using Shiny;
[ShinyJsonContext]
[JsonSerializable(typeof(MyDto))]
[JsonSerializable(typeof(MyOtherDto))]
internal partial class MyAppJsonContext : JsonSerializerContext;
```
3. Done. The generator emits a `[ModuleInitializer]` calling `Shiny.Json.AddContext(MyAppJsonContext.Default)` before `Main`, so both DI consumers and the static `Shiny.Json.Default` accessor see your types — no `services.AddJsonContext(...)` call needed.
4. (Optional) For collection support, mark element types with `[ShinyJsonInclude]`:
```csharp
[ShinyJsonInclude]
public partial class MyDto { /* ... */ }
```
`List<MyDto>`, `MyDto[]`, `IEnumerable<MyDto>` and friends now serialize AOT-safely.
```csharp
// Static access — works before DI exists (useful for mobile cold-start through Shiny.Stores)
var json = Shiny.Json.Default.Serialize(new MyDto { Name = "Allan" });
// DI access — same shared instance
public class MyService(ISerializer serializer) { /* ... */ }
```
## Stores
* Cross-platform key/value store with support for
* Android/iOS/Windows - Preferences & Secure Storage
* Web - Local Storage
* In Memory
* Source-generated `[Bind]` attribute on partial properties - emits getter/setter bodies that round-trip through the store (no INPC required, no runtime reflection, fully AOT)
* Static `Shiny.Stores.Default/Secure/Keyed(...)` accessor for direct ad-hoc reads/writes
* Implement `IKeyValueStore` to plug in your own store
### Setup
1. Install the NuGet package `Shiny.Extensions.Stores`
2. Register at startup — the static `Shiny.Stores` accessor self-bootstraps on first use:
```csharp
builder.Services.AddShinyStores();
var host = builder.Build();
```
On Blazor WebAssembly (where `LocalStorageKeyValueStore` needs `IJSRuntime`), also call `host.Services.UseShinyStores()` after `Build()` to snapshot the DI-resolved store into the static accessor.
`AddShinyStores()` registers `IKeyValueStore` keyed under `StoreKeys.Default` and `StoreKeys.Secure`, **plus the default store unkeyed**. The unkeyed registration exists for third-party container adapters that predate .NET 8 keyed services — Prism's DryIoc container, for example, still sits on DryIoc 5.x, silently ignores `[FromKeyedServices]`, and resolves the plain `IKeyValueStore` instead. On those containers, use the static `Shiny.Stores.Secure` / `Shiny.Stores.Keyed(...)` accessor when you need a non-default store, since the key will be dropped.
3. Define your settings as a `partial` class with `[Bind]` partial properties:
```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; }
}
```
4. Inject `AppSettings` anywhere. Set properties — they persist. Read properties — they come from the store.
Or skip the class and use the static accessor:
```csharp
Shiny.Stores.Default.Set("theme", "dark");
var theme = Shiny.Stores.Default.Get<string>("theme");
```
### Available Stores Per Platform
| Platform | Key | Description |
|--------------|----------------------|-------------------------------------|
| Android | `StoreKeys.Default` | Preferences |
| Android | `StoreKeys.Secure` | Secure Storage |
| iOS | `StoreKeys.Default` | NSUserDefaults |
| iOS | `StoreKeys.Secure` | Keychain |
| Windows (packaged) | `StoreKeys.Default` | ApplicationData.LocalSettings |
| Windows (packaged) | `StoreKeys.Secure` | Secure Storage (DPAPI) |
| Windows (unpackaged) | `StoreKeys.Default` | JSON file (`LocalApplicationData`) |
| Windows (unpackaged) | `StoreKeys.Secure` | JSON file + DPAPI encryption |
| macOS (`net10.0-macos`) | `StoreKeys.Default` | NSUserDefaults |
| macOS (`net10.0-macos`) | `StoreKeys.Secure` | Keychain |
| Linux / other desktop | `StoreKeys.Default` | JSON file (`LocalApplicationData`) |
| Linux / other desktop | `StoreKeys.Secure` | JSON file (**not encrypted**) |
| WebAssembly | `StoreKeys.Default` | localStorage |
| All | any | In-memory dictionary (via `MemoryKeyValueStore`, great for testing) |
> [!NOTE]
> For Blazor WebAssembly, add `services.AddLocalStorageKeyValueStore()` (and optionally `services.AddLocalStorageRepository()`) to your service collection, then call `host.Services.UseShinyStores()` after `builder.Build()`. These ship in `Shiny.Extensions.Stores` itself (the separate `Shiny.Extensions.Stores.Web` package has been merged in) and call `window.localStorage` directly through `IJSInProcessRuntime` — no `<script>` tag is needed.
> [!NOTE]
> A dedicated `net10.0-macos` target gives plain macOS apps NSUserDefaults + Keychain (real secure storage). Other desktop targets that resolve the base `net10.0` asset (Linux, and unpackaged Windows) persist both `Default` and `Secure` to a JSON file under `{LocalApplicationData}/{EntryAssemblyName}` so settings survive restarts. Override the location by setting `Shiny.Stores.FileStoreDirectory` before first access. On these file fallbacks `Secure` is **not** encrypted (unpackaged Windows keeps DPAPI over the file) — treat it as obfuscation, not protection, for sensitive data.
> [!NOTE]
> On Apple platforms the default store is scoped to the app's **own** preferences domain. `NSUserDefaults.StandardUserDefaults` is a search list — it also resolves through the global domain and through defaults any linked framework registered — so `Contains`/`Get` deliberately ask the app's persistent domain instead. Without that, an ordinary key name (`AutoRecord`, `UseMetric`, `Enabled`) can read back as *present* having never been written, and `Get(key, defaultValue)` would hand back that foreign value instead of your default.
## MAUI Hosting Extensions
* Module-based MAUI app configuration with `IMauiModule`
* Static `ShinyHost.Services` for accessing the service provider anywhere
* `IAppSupport` — browser/map launch, programmatic orientation lock, and live orientation change events. Device info, culture and time zone (with change events) and opening the app's settings live on Shiny's `IPlatform` in Shiny.Core, implemented natively without MAUI
* `IAppStore` moved to the `Shiny.AppStore` package (namespace `Shiny.AppStore`), registered with `builder.Services.AddAppStore(...)` — see https://shinylib.net/client/appstore/. Do not generate it against this library
* Launch at login moved to Shiny.Core as `IAppStartupInstaller`, registered with `services.AddAppStartupInstaller(...)` ([docs](https://shinylib.net/client/core/app-startup-installer)) - never generate `IStartupService` / `AddStartupService` against this library
* Desktop backends from [dotnet/maui-labs](https://github.com/dotnet/maui-labs): macOS AppKit (`net10.0-macos`) is served by this package, Linux GTK4 by the companion `Shiny.Extensions.MauiHosting.Linux` package
* Opt-in registration: each capability is its own extension method so apps only pay for what they use
### Setup
1. Install the NuGet package `Shiny.Extensions.MauiHosting`
2. Create a MAUI module by implementing `IMauiModule`:
```csharp
using Shiny;
public class MyMauiModule : IMauiModule
{
public void Add(MauiAppBuilder builder)
{
// Register your services here
}
public void Use(IPlatformApplication app)
{
// Post-build initialization here (do NOT block)
}
}
```
3. In your `MauiProgram.cs`:
```csharp
using Shiny;
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.AddInfrastructureModules(new MyMauiModule()) // your IMauiModule list
.AddAppSupport(); // IAppSupport
return builder.Build();
```
4. Access services anywhere via `ShinyHost.Services`
#### IAppSupport
```csharp
public class MyVm(IAppSupport app)
{
void Hook()
{
// Snapshot - device info, culture and time zone are on Shiny.Core's IPlatform
var orientation = app.CurrentOrientation;
// Live updates
app.OrientationChanged += (s, e) => { /* new DisplayOrientation */ };
// Programmatic orientation lock - call once a page has appeared: UIKit ignores a change
// made while a navigation transition is animating
var turned = await app.SetOrientation(DisplayOrientation.Landscape);
_ = app.ResetOrientation();
}
}
```
On iOS, `SetOrientation` turns the device, but UIKit asks the **app delegate** which orientations a
window may take - so to *hold* it there, return `IosOrientationLock.Supported` from yours. Without
this the device turns once and tilting it turns it straight back.
```csharp
[Export("application:supportedInterfaceOrientationsForWindow:")]
public UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow? forWindow)
=> Shiny.IosOrientationLock.Supported;
```
> `IAppSupport` needs extra setup on the desktop heads — see the desktop backend section below.
### Desktop backends (macOS AppKit + Linux GTK4)
.NET MAUI has no first-party macOS (AppKit) or Linux head. [dotnet/maui-labs](https://github.com/dotnet/maui-labs)
ships both as experimental preview packages, and this library supports them.
#### macOS (AppKit)
The `net10.0-macos` asset of `Shiny.Extensions.MauiHosting` carries `IAppSupport`. On that head MAUI Essentials resolves its platform-neutral asset,
where every member throws `NotImplementedInReferenceAssembly`, so the package depends on
`Microsoft.Maui.Platforms.MacOS.Essentials` and `AddAppSupport()` calls `AddMacOSEssentials()`
for you.
```csharp
using Microsoft.Maui.Platforms.MacOS.Hosting;
using Shiny;
var builder = MauiApp.CreateBuilder();
builder
.UseMauiAppMacOS<App>()
.AddInfrastructureModules(new MyMauiModule())
.AddAppSupport(); // wires the AppKit Essentials implementations in
return builder.Build();
```
Orientation comes through
`DeviceDisplay.MainDisplayInfoChanged` (`NSApplication.DidChangeScreenParametersNotification`).
`SetOrientation`/`ResetOrientation` return false — AppKit windows don't rotate.
#### Linux (GTK4)
Linux has no `-linux` TFM, so a GTK4 head is a plain `net10.0` project. The base package's `AddAppSupport()`
would resolve that same throwing Essentials asset, and `AddLinuxGtk4Essentials()` only redirects five of the
static APIs — so Linux gets its own package, `Shiny.Extensions.MauiHosting.Linux`, whose implementations
resolve the Essentials *interfaces* out of the container instead.
```csharp
using Microsoft.Maui.Platforms.Linux.Gtk4.Hosting;
using Shiny;
var builder = MauiApp.CreateBuilder();
builder
.UseMauiAppLinuxGtk4<App>()
.AddInfrastructureModules(new MyMauiModule())
.AddLinuxAppSupport(); // IAppSupport over the GTK4 Essentials services
return builder.Build();
```
`AddLinuxAppSupport()` is named apart from the base `AddAppSupport()` on purpose — both packages put their
extensions on `MauiAppBuilder` in the `Shiny` namespace, and only one of the two works on Linux.
| Capability | Linux behaviour |
|------------|-----------------|
| Browser / map | The GTK4 Essentials services (`IBrowser` and `IMap` over `xdg-open`) |
| Orientation | Reported from the GDK monitor geometry; `SetOrientation`/`ResetOrientation` return false |
> Both backends are `0.1.0-preview` packages from dotnet/maui-labs and are not covered by the .NET MAUI
> support policy. `samples/Sample.Maui.MacOS` and `samples/Sample.Maui.Linux` are runnable heads of the
> shared sample app for each.
## Blazor WebAssembly Hosting Extensions
* `IAppSupport` for Blazor WebAssembly — screen and viewport dimensions. App version, the browser user agent and parsed browser version, and culture / time-zone change events are on Shiny.Core's `IPlatform` / `NetPlatform`
* Reads browser state synchronously through `IJSInProcessRuntime` (same approach as the `Shiny.Extensions.Stores` localStorage store)
### Setup
1. Install the NuGet package `Shiny.Extensions.BlazorHosting`
2. Reference the bundled script in `wwwroot/index.html` **before** `blazor.webassembly.js`:
```html
<script src="_content/Shiny.Extensions.BlazorHosting/shiny-appsupport.js"></script>
```
3. Register at startup:
```csharp
using Shiny;
builder.Services.AddAppSupport();
```
4. Inject `IAppSupport` anywhere:
```csharp
public class MyComponent(IAppSupport app)
{
void Hook()
{
var (w, h) = (app.BrowserWidth, app.BrowserHeight); // viewport — read live
var (sw, sh) = (app.ScreenWidth, app.ScreenHeight); // physical screen
}
}
```
## Additional Libraries Used
* [Shiny Reflector](https://github.com/shinyorg/reflector) - Reflection without the actual reflection
## NuGet Packages
| Package | Description |
|---------|-------------|
| `Shiny.Extensions.DependencyInjection` | Attribute-driven DI registration with source generators |
| `Shiny.Extensions.Serialization` | Centralized AOT-safe JSON serializer + `[ShinyJsonContext]`/`[ShinyJsonInclude]` source generator |
| `Shiny.Extensions.Stores` | Cross-platform key/value store abstraction (includes Blazor WebAssembly localStorage) |
| `Shiny.Extensions.MauiHosting` | MAUI modular hosting with `IMauiModule` and platform lifecycle hooks (includes the macOS AppKit head) |
| `Shiny.Extensions.MauiHosting.Linux` | `IAppSupport` for the MAUI Linux GTK4 backend |
| `Shiny.Extensions.BlazorHosting` | Blazor WebAssembly `IAppSupport` — screen and viewport dimensions |
## Commercial Support
Shipping this to production? [ShinySoft Technologies](https://shinysoft.biz), the company that builds and maintains the Shiny libraries, offers priority issue response, support agreements, architecture reviews and private office hours. See [Commercial Support](https://shinylib.net/foundation/support/?utm_source=github&utm_medium=readme#commercial-support) or [get in touch](https://shinysoft.biz/contact?utm_source=github&utm_medium=readme). Community support through GitHub issues and discussions stays free.