A suite of powerful .NET libraries for mobile, desktop, and server applications
Developer toolsC# · JavaScriptGitHub @shinyorgOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Loading comments.
Checking sign-in status.
.NET Framework for Backgrounding & Device Hardware Services (iOS, MacCatalyst, Android, & Windows)
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Shiny is a cross-platform framework designed to make working with device services and background processes easy, testable, and consistent while bringing
things like dependency injection & logging in a structured way to your code!Read the full README ↗Shiny is a cross-platform framework designed to make working with device services and background processes easy, testable, and consistent while bringing things like dependency injection & logging in a structured way to your code!
Every package ships under the same version. Blazor WebAssembly support ships inside each package's net10.0 build - there are no *.Blazor packages. Linux add-ons that talk to D-Bus (*.Linux) ship next to the packages below so that dependency never reaches a WebAssembly app.
| Area | Modules | Packages |
|---|---|---|
| Foundation | Core - hosting, DI, stores, lifecycle, connectivity & battery | Shiny.Core, Shiny.Hosting.Maui (iOS, Android, Mac Catalyst, macOS, Windows, Linux GTK), Shiny.Hosting.Native |
| Background Work & Data | Background Jobs, HTTP Transfers, Data Sync | Shiny.Jobs, Shiny.Net.Http, Shiny.Data.Sync |
| Notifications & Live Surfaces | Local Notifications, Push Notifications, Live Activities | Shiny.Notifications, Shiny.Push, Shiny.Push.AzureNotificationHubs, Shiny.Mobile.LiveActivities |
| Bluetooth | BluetoothLE Client, BluetoothLE Hosting, Beacons | Shiny.BluetoothLE, Shiny.BluetoothLE.Hosting, Shiny.Beacons |
| Networking | Network Discovery (mDNS, SSDP, WS-Discovery), Wi-Fi, VPN | Shiny.Net.Discovery, Shiny.Net.Wifi, Shiny.Net.Vpn |
| Location | GPS, Geofencing, Region Geofencing | Shiny.Locations (or Shiny.Gps / Shiny.Geofencing individually), Shiny.Geofencing.Regions, Shiny.Geofencing.Regions.DocumentDb |
| Personal Data | Contacts, Calendar, Call Directory (call blocking & caller ID) | Shiny.Contacts, Shiny.Calendar, Shiny.Mobile.CallDirectory |
| Devices & Peripherals | Printing, Gamepads, Wearables, Screen Recording, Biometrics, NFC | Shiny.Printers, Shiny.Printing, Shiny.Gamepad, Shiny.Wearables, Shiny.ScreenRecorder, Shiny.Biometrics, Shiny.Nfc |
| Commerce | In-App Purchases - App Store & Google Play, plus the ASP.NET Core backend; App Store - update checks, store links & review prompts; Age Signals - the store-shared age range for app store age laws | Shiny.Mobile.InAppPurchases, Shiny.Mobile.InAppPurchases.Server, Shiny.AppStore, Shiny.AgeSignals |
| Games | Game Center - achievements, leaderboards & friends over Apple Game Center and Google Play Games | Shiny.GameCenter |
| AI & Assistants | App Functions, AI Tools | Shiny.AppFunctions, Shiny.*.Extensions.AI |
IPlatform directories and main thread, plus natively read device and app info (version, application identifier, manufacturer, model, OS version, platform, form factor), culture and time-zone change events and OpenAppSettings(), with no MAUI dependency, and IAppStartupInstaller to put the app in the OS launch-at-login list (Windows Run key, macOS/Mac Catalyst login items, Linux XDG autostart or a systemd user/system unit) (docs)IScreenInfo in Shiny.Core reports the main display (physical size, density, refresh rate, portrait/landscape) with change events, and locks orientation (SetOrientation / ResetOrientation) on Android, iOS 16+, Windows tablets and Blazor full-screen pages - natively on every platform with no MAUI, registered by every host. Replaces the orientation and screen-size members of the old IAppSupportIScreenPosture in Shiny.Core reports whether a fold or hinge divides the window, where it is (Fold in window dp/points), whether it separates content, tabletop / book posture and the hinge status - Jetpack WindowManager on Android foldables, the hinge and reserved-region APIs on iPhone Duo (iOS 27.1+, including the camera housings as Occlusions and the hinge angle), and the Device Posture / Viewport Segments APIs in Chromium on Blazor WebAssembly. Segments(windowSize) returns the two panes either side of the fold. Registered by every host; elsewhere it reports ScreenPosture.NoneAndroidPlatform.StartActivityForResult(intent) starts an activity from the current one and awaits its ActivityResult (role requests, settings screens, consent dialogs), and ActivityResultReceived raises every result the host forwards. Shiny.Hosting.Maui forwards results automatically and Shiny.Hosting.Native through ShinyAndroidActivity, so a module never needs a result activity of its ownBGTaskScheduler (iOS/tvOS), WorkManager (Android), COM-activated background tasks (Windows), and an in-process managed runner for Linux/macOS/Blazor WASMNSURLSession (iOS/tvOS), an HttpClient-driven managed loop with Range-based resume (Android, Windows, Linux, macOS, .NET base), and Service Worker Background Sync (Blazor WASM). Pause/resume support - pause stops a transfer without cancelling it (downloads continue from where they left off, uploads restart - except tus resumable uploads, which continue from the server's offset after a pause, network drop or app restart on every platform but Blazor). First-class Azure Blob Storage, AWS S3 (SigV4) and tus (TusUploadRequest) request builders included. AddTransferProgress() puts progress in front of the user - an iOS Live Activity, or the Android foreground-service notification promoted to an Android 16 live update - from one manager, with no code in your transfer delegateNSURLSession (iOS/tvOS/Mac Catalyst), Foreground Service + HttpClient (Android), HttpClient + connectivity loop (Windows/Linux/macOS), and LocalStorage-backed HttpClient (Blazor WASM). Includes batched outbox, tombstones, conflict resolution, retry with exponential backoff, and AOT-safe serialization through Shiny.Jsonorg.freedesktop.Notifications D-Bus). Linux has no OS scheduler, so scheduled and repeating notifications fire from an in-process timer while the host runs - an IntervalTrigger.TimeOfDay is local time and fires today if that time is still ahead. TrySetBadge / TryGetBadge set the app icon badge on iOS/Mac Catalyst and the Dock tile badge on macOS, and return false on Android and Windows, which have no app-wide badge. ChannelAction.RequiresUnlock makes an action button ("Open Garage", "Disarm") ask for the device to be unlocked before it runs, instead of running straight from the lock screen - iOS/macOS and Android 12+. Notification.ImageUri (an https URL or a local file) shows an image with the notification - an attachment on iOS/macOS, a big picture on Android, the hero image on Windows - downloaded when the notification is sent, so scheduled ones need no network when they fire, and a missing image never stops the notificationOnEntry never fires there. Azure Notification Hubs covers iOS, Mac Catalyst, macOS, tvOS, Android and Windows. Push images: on iOS, <ShinyPushServiceExtension>true</ShinyPushServiceExtension> builds a Notification Service Extension into your app during dotnet build - no Xcode project, Swift or Info.plist edits, its provisioning profile inferred and signed like the app's - that downloads the image URL in a push sent with "mutable-content": 1 (image, image-url, attachment-url or Firebase's fcm_options.image by default; ShinyPushImageKeys changes them) and attaches it. The extension is plain Swift with no .NET runtime, so it fits iOS's ~24 MB extension budget, and ShinyPushServiceExtensionSource swaps in your own. On Android, FCM shows a notification message's image itself in the background, and AndroidPushNotification.SendDefaultAsync(id) adds it as a big picture in the foreground. The Web Push service worker shows the payload's image in Chromium browsers. Notification.ImageUri reports the URL on every platformawait peripheral.UploadFile(psm, path, onProgress: ...) / DownloadFile(...) - with the same transfer metrics as HTTP Transfers (percent complete, bytes/sec, ETA), plus Rx *WithProgress variants. channel.AsStream() hands an open L2CAP channel to anything that takes a System.IO.Stream. Against a host that shares one PSM between many transfers, await peripheral.OpenL2CapTicketChannel(psm, token) opens the channel, presents the single-use ticket the host issued, and returns the stream once the host accepts it - a refusal throws L2CapTicketException naming why. WriteCharacteristicMessageAsync(...) / NotifyCharacteristicMessages(...) carry messages longer than one GATT operation, split to the MTU with a one-byte header and reassembled on the far side, so a JSON command or a certificate needs no protocol of your own. IPeripheral.Mtu reports the usable payload per GATT operation (the negotiated ATT MTU minus the 3-byte ATT header) on every platform - fragment writes to it directly. Writes without response on Apple wait on CoreBluetooth's flow control without ever missing its ready callback, so a full send buffer paces the stream instead of wedging the peripheral's operation queue. That wait runs outside the shared GATT operation queue and gives up after 5 seconds, so a peripheral that stops draining its buffer fails its own writes with a BleException while reads, notifications and other writes carry on. A GATT operation that a disconnect fails after its subscriber has already gone - the usual case when a WhenStatusChanged()-driven pipeline tears down on the same drop - is dropped instead of surfacing through TaskScheduler.UnobservedTaskException, so a cleanly handled disconnect no longer lands in crash telemetry. ConnectionConfig(AutoConnect: true) - the default - keeps the link up on every platform, re-establishing it after a peripheral drops out of range or is power-cycled. On iOS/Mac Catalyst/macOS and Android it also survives the user toggling Bluetooth off and on - an adapter power-down runs the same teardown a real disconnect gets, so WhenStatusChanged() never lies about a dead link, connects issued while the adapter is off are parked and replayed rather than silently dropped, and a scan started while the link is down no longer evicts the peripheral waiting to reconnect. Scan() is parked the same way: on Apple platforms a CoreBluetooth central reports Unknown until it finishes powering on, and a scan issued into that window used to be dropped without a word - it now starts as soon as the adapter is up, and resumes by itself after a power cycle. stays final. On iOS/Mac Catalyst the three system alerts CoreBluetooth can raise for a peripheral while the app is out of the foreground (" would like to open " on connect, disconnect or every notification) are now / / - still on by default, and worth turning off in an app that connects from the background deliberatelyNSNetService on iOS/tvOS/Mac Catalyst/macOS (so no com.apple.developer.networking.multicast entitlement is needed), NsdManager on Android (no multicast lock), and a dependency-free managed responder on UDP 5353 for Windows, Linux, macOS console, and server .NET. await foreach (var r in mdns.Browse("_http._tcp", ct)) for live discovery, BrowseOnce(...) for a one-shot scan, and Publish(...) to advertise your own service with TXT metadatassdp.SearchAll() for a one-shot sweep, Browse(...) for a live list keyed on UDN with ssdp:alive/byebye tracking and BOOTID-aware expiry, GetDescription(...) to fetch and parse the device description (friendly name, model, icons, service list), and Publish(...) to advertise your own root device. Discovery and description only - no SOAP action invocation, no GENA eventingTypes, RFC 3986 segment-prefix scope matching, and ProbeOnvifCameras(...) as a shortcutcom.apple.developer.networking.multicast entitlement, Android needs CHANGE_WIFI_MULTICAST_STATE (the multicast lock is acquired for you) plus ACCESS_LOCAL_NETWORK from Android 17. Sandboxed hosts still need their usual network permission - com.apple.security.network.client/.server on Mac Catalyst & macOS, privateNetworkClientServer for packaged Windows apps. A missing one throws DiscoveryPermissionException naming exactly what to add, rather than silently finding nothingShiny.Gps + Shiny.Geofencing; Shiny.Locations is a meta-package that references both) - foreground/background GPS, geofence monitoring with enter/exit/dwell (dwell is native on Android, a timer on Windows, and calculated from the entry/exit on iOS, where it usually arrives at exit), reverse geocoding on every platform (IGeocoder - MapKit/CoreLocation on iOS/Mac Catalyst, Geocoder on Android, and the free OpenStreetMap Nominatim API on Windows, Linux, macOS, Blazor WebAssembly and Android devices without Play Services - rate-limited to its one-request-a-second policy and cached), and motion-activity recognition (CMMotionActivity / ActivityRecognition). Background GPS listeners and the motion-activity listener are persisted and re-registered at startup when the OS ends the process, so check CurrentListener / IsListening before starting one
RequestAccess now escalates with it. A CLServiceSession is bound to the requirement it was opened under, so the when-in-use session a foreground request creates is replaced (not reused) when a background request later needs always - an existing always session still covers a foreground request, so a background grant is never torn down to ask for less. Ask from the feature that needs it: the system presents the "always" upgrade prompt once, and that moment is the only one where you can say why. AccessState.Restricted is what when-in-use reports as when a background request asks about it - "granted, but not for what you asked for", not "blocked"Shiny.Geofencing.Regions) - GPS-driven geofencing with no region limit (iOS, Android, Mac Catalyst). Background GPS readings are filtered by distance/time and resolved against region sets, each tracking its own current region across restarts. Regions come from a pluggable IRegionSource: built-in reverse geocoding (country, state/province, county, city, neighbourhood or postal code - online only, no data to ship), or Shiny.Geofencing.Regions.DocumentDb - polygon (containment) and point (proximity) documents in a Shiny.DocumentDb SQLite store, with a seeder for US states, Canadian provinces and US & Canadian cities. Replaces Shiny.DocumentDb.Geofencing, Shiny.DocumentDb.Geo and Shiny.Spatial.GeofencingContactsContract): await store.Query().Search(text).OrderBy(ContactSortField.FamilyName).ToListAsync(ct)AppointmentStore, best-effort - system events are read-only, writes go to an app-owned calendar). await store.Query().ForCalendar(id).Between(from, to).OrderBy(CalendarEventSortField.Start).ToListAsync(ct) - the calendar id and date window are pushed down to the native fetch, which runs off the calling thread. DeleteEvent(id, deleteSeries) chooses between removing one occurrence of a recurring event or the rest of the seriesICallDirectoryManager. iOS: the CallKit Call Directory - <ShinyCallDirectoryExtension>true</ShinyCallDirectoryExtension> plus <ShinyCallDirectoryAppGroup> builds the Call Directory extension during dotnet build (no Xcode project) with its own provisioning profile on device builds; the app writes Block / Identify entries to a SQLite store in the app group and Reload() has iOS load them, incrementally - only numbers changed since the last load iOS accepted are sent. Android 10+: the call screening role (RequestAccess() shows the system "caller ID & spam app" dialog) and a CallScreeningService that ships in the package check every incoming call against the same list - blocking, an Allow list, optional BlockUnlistedCalls, and a caller ID notification for Identify entries (apps cannot change the dialer's caller ID). ReplaceAll swaps in a whole server-synced list in one transaction and PhoneNumbers.ToCallDirectory normalizes what a person typed. The store uses the platform's own SQLite (no SQLitePCLRaw), so it is trim/AOT safe. Elsewhere IsSupported is false and every call is a safe no-op (docs)Shiny.Printers) - build a protocol-neutral PrintDocument fluently (styled text, alignment, 1D barcodes, QR codes, raster images, paper cut, raw bytes) and an IPrinterProtocol encodes it - EscPosProtocol in full, plus a minimal TsplProtocol for label printers. PrinterCapabilities carries the "driver constants" a receipt is laid out against (CharactersPerLine, DotsPerLine, cutter), with Paper58mm/Paper80mm presets. PrinterImage.FromPixels does grayscale + threshold/Floyd-Steinberg dithering + 1bpp packing; decoding the PNG is the caller's job, so the core has no imaging dependency and is AOT/trim cleanShiny.Printers.BluetoothLE scans for and connects to BLE printers on the common ESC/POS GATT fingerprints (18F0/2AF1, HM-10 FFE0/FFE1, ISSC) with MTU-chunked writes; Shiny.Printers.Network streams raw TCP to port 9100 (JetDirect) and finds printers over mDNS through Shiny.Net.Discovery, or with Sweep() - a port-9100 sweep of the local subnet for the many receipt printers that never advertise - and reads paper, cover, cutter and cash-drawer state back with GetStatus() (ESC/POS DLE EOT; SupportsStatus is false on the write-only BLE and browser transports); Shiny.Printers itself drives Web Bluetooth, Web Serial and WebUSB from the browser (AddBrowserPrinting()), where the device chooser replaces the scan. Dispose the Printer a manager hands back to disconnectShiny.Printing) - IPrintService.Print(PrintJob.Pdf/Image/Html/File(...)) hands content to the platform pipeline so any installed printer works. HTML prints through a real browser engine (WKWebView / WebView) so CSS layout and page breaks survive, and IPrintService.HtmlToPdf lays HTML out as a PDF to share instead of print on iOS, Mac Catalyst and Android: AirPrint on iOS/Mac Catalyst, PrintManager on Android, GDI+ and the shell print verb on Windows, and CUPS (lp/lpstat) on Linux and macOS. Silent printing to a named printer and printer enumeration where the OS allows it - feature-detect with rather than catching. On Blazor, prints through IInAppPurchaseManager: StoreKit 2 on iOS (through a bundled Swift shim - StoreKit 2 is Swift-only, and StoreKit 1 is deprecated) and Google Play Billing 9 on Android. Products, purchases, entitlements, restore and subscription management, including Google base plans, offers and free trials and Apple introductory offers. Pending purchases (Ask to Buy, slow payment methods), renewals, refunds and purchases made on other devices reach your IPurchaseDelegate from app launch. Nothing auto-finishes - verify Purchase.VerificationData on your server, grant, then FinishPurchaseAsync(purchase, consume), so a crash between payment and granting never loses a purchase (Google refunds anything unacknowledged after 3 days). PurchaseOptions.AccountToken (a Guid) ties purchases to your user and comes back in server notifications. Shiny.Mobile.InAppPurchases.Server is the ASP.NET Core half: AddInAppPurchaseServer(...) + MapInAppPurchaseWebhooks("/iap") receive App Store Server Notifications V2 (JWS verified offline against Apple Root CA G3) and Google Play RTDN over Pub/Sub push (OIDC-authenticated) as one PurchaseEvent stream for your IPurchaseEventHandlers, with de-duplication and store retries (a throwing handler returns a 5xx); MapInAppPurchaseVerification and IPurchaseVerifier verify purchases the app reports, and IAppleStoreClient / IGooglePlayClient wrap the App Store Server API and Play Developer API. BCL-only crypto, AOT-compatible. iOS and Android only - this is not Apple Pay / Google Pay card processing, which goes through a payment processor (docs)IAppStore asks the store the app ships through whether a newer version is published (GetCurrent() - store and running versions, NeedsUpdate, plus release notes, release date, ratings and minimum OS where the store gives them), opens the store and review pages, and requests the native in-app review prompt. The Apple App Store and Mac App Store (iTunes lookup, StoreKit prompt), Google Play (Play in-app review), the Microsoft Store (DisplayCatalog, StoreContext), and Flatpak/Snap on Linux (appstream:// software-centre links), all implemented natively with no MAUI. Register with services.AddAppStore() ()IGameCenterManager (skill)
AddGameCenter(o => o.AddAchievement("first_win", appleId: "...", googleId: "CgkI...").AddLeaderboard("high_score", ...)) maps your logical keys to each store's ids once; unregistered keys pass straight through as platform ids. An achievement registered for only one store is skipped on the otherUnlockAsync, SetProgressAsync, IncrementAsync, RevealAsync and SubmitScoreAsync record to a durable JSON queue first and then send, so an offline or signed-out player loses nothing - the queue drains on sign-in, when connectivity returns and after every call, and survives restarts. Progress is held in steps and sent as an absolute value (Apple percent / Google setSteps), so a retry after a crash is idempotent and calls that would not move an achievement forward are skipped. Queued work stays with the player who earned it when accounts switch on a shared device; work recorded before anyone signed in goes to whoever signs in firstGetScoresAsync(key, LeaderboardScope.Friends, LeaderboardTimeScope.Week) loads global or friends-only scores; GetFriendsAsync() returns the friends visible to the game after the platform consent prompt (FriendsAccessStatus reports Denied / Restricted / Google's ConsentRequested)GKAccessPoint (GKGameCenterViewController is deprecated in the 26 SDKs; it remains only for single-leaderboard views on iOS 15-17). Play Games v2 through Xamarin.GooglePlayServices.Games.V2, using the *Immediate calls so the queue only clears once Google's servers have the data; PlayersClient is not in that binding and is reached through JNI. IGameServicesProvider is public, so another service (Steam) can plug in behind the same managercom.apple.developer.game-center entitlement, Game Center enabled on the App Store Connect version, and NSGKFriendListUsageDescription for friends. Android: manifest meta-data and the SHA-1 of every signing key (debug, upload, Play App Signing) registered as OAuth credentials[AppFunction("create_order")] record implementing IAppFunction<TResult> is the request, an IAppFunctionHandler<TRequest, TResult> runs it in its own DI scope, [AppEntity] records resolved through an IAppEntityQuery<T> become pickable parameters (plus a generated search_{entity} function on Android, which has no entity queries), and IAppFunctionDelegates gate every call (AppFunctionGate.Allow / Deny / OpenApp) and observe its outcome. context.IsForeground tells a handler the user asked from inside the app (a Siri or Gemini overlay, on both platforms) - it runs in the live app, so it can update the screen, and an OpenApp gate passes without a prompt. [AppFunction(OpensApp = true)] marks a function that needs the app on screen: iOS brings the app forward first, and Android runs it only while the app is visible and otherwise refuses it. A bundled source generator writes services.AddAppFunctions(), reflection-free binding and dispatch, the Swift App Intents (with [AppShortcut] Siri phrases) and the Android AppFunctions schema; its build task compiles the Swift into the app (on a macOS build host) and adds the schema assets - no Swift, Kotlin, plist or manifest entries to write. Functions must be declared in the app project. iOS and Android; the base target carries the contracts and the dispatcher, Shiny.AppFunctions.Extensions.AI hands the same functions to an in-app LLM as tools (see AI Tools), and every function's JSON schema is exposed for MCP adaptersMicrosoft.Extensions.AI tools - see AI Tools belowShiny.Core, Shiny.Hosting.Native, Shiny.BluetoothLE, Shiny.Net.Discovery, Shiny.Jobs, Shiny.Net.Http, Shiny.Push, Shiny.ScreenRecorder, Shiny.Gamepad and Shiny.Data.Sync ship a net10.0-tvos target. tvOS reuses the iOS platform layer wholesale - the same ShinyAppDelegate, IosPlatform, IosLifecycleExecutor and IIosLifecycle hooks - so there is no tvOS-specific code to write. There is no MAUI on tvOS, so a tvOS head hosts through Shiny.Hosting.Native.
What Apple withholds on tvOS, and what Shiny does about it:
CBMutableService and CBMutableCharacteristic have no constructors on tvOS, so Shiny.BluetoothLE.Hosting has no tvOS target. The central role is complete, L2CAP includedbluetooth-central background mode, so scans and connections end with app suspension and RestoreIdentifier has nothing to restoreUNNotificationContent carries no title, body or user info and there is no UNNotificationResponse, so IIosLifecycle.INotificationHandler does not exist on tvOS and Shiny.Push raises OnReceived (silent push) but never OnEntryScreenRecorderCapabilities.Microphone is not advertised on tvOS; everything else ReplayKit does on iOS it does on tvOSIBattery reports permanently full, on AC, energy saver off - an Apple TV is mains powered and UIDevice carries no battery API thereShiny.Gamepad reports it as a GCMicroGamepad with GamepadKind.Remote - no sticks, no triggers, two buttons - and maps its analog touch surface onto the left stick, so menu code written against a controller works unchanged. A real MFi controller paired to the Apple TV reports everything it would on iOSNEHotspotConfiguration/NEHotspotNetwork and CLMonitor are absent, so Shiny.Net.Wifi, Shiny.Gps and Shiny.Geofencing have no tvOS targetsA working UIKit tvOS app exercising all of it lives in samples/Sample.tvOS.
Optional *.Extensions.AI packages expose Shiny modules as Microsoft.Extensions.AI tool functions (AIFunctions) for LLM agents. You opt-in exactly which operations the model can see - a read/write allow-list you control on behalf of the agent (this is not an OS permission prompt; the underlying platform permissions must already be granted). Resolve the generated *AITools bundle from DI and pass .Tools to any IChatClient. All are AOT-compatible (hand-built schemas, no reflection).
AddContactsAITools(...) → search_contacts, get_contact, and (write) create_contact, update_contact, delete_contactAddCalendarAITools(...) → list_calendars, search_events, get_event, and independently opt-in create_event, update_event, delete_event (per-operation Read/Create/Update/Delete capability flags). Capabilities apply to every calendar by default, or per calendar id - AddCalendar(workId, All) alongside a global Read gives the agent read-only access everywhere but read/write on one calendar, and None hides a calendar entirelyAddNotificationAITools(...) → reminder-framed list_reminders, and (write) create_reminder (one-time or daily), cancel_reminderAddLocationAITool() → read-only get_current_location, get_distance_to, estimate_travel_timeAddAppFunctionAITools(...) → your own App Functions - the ones Siri and Gemini call - as tools, named by their function ids (create_order, search_customer, …). Opt in with AddAllFunctions() or AddFunction("create_order") (a function with an entity parameter brings its search_{entity} function along) and hide some with ExcludeFunction(...). Calls run through AppFunctionDispatcher, so binding, your IAppFunctionDelegate gates and error mapping are the same as for Siri and Gemini; the calls count as foreground (the chat runs in the app), so OpensApp functions run and OpenApp gates passbuilder.Services.AddContactStore();
builder.Services.AddContactsAITools(b => b.AddContacts(ContactAICapabilities.ReadWrite));
builder.Services.AddNotifications();
builder.Services.AddNotificationAITools(b => b.AddReminders(ReminderAICapabilities.ReadWrite));
builder.Services.AddGps();
builder.Services.AddLocationAITool();
builder.Services.AddAppFunctions(); // source-generated
builder.Services.AddAppFunctionAITools(b => b.AddAllFunctions().ExcludeFunction("cancel_order"));
// later, hand the tools to a chat client
var tools = sp.GetRequiredService<ContactAITools>().Tools;
var response = await chatClient.GetResponseAsync(messages, new ChatOptions { Tools = [.. tools] });
Shipping this to production? ShinySoft Technologies, the company that builds and maintains the Shiny libraries, offers priority issue response, support agreements, architecture reviews and private office hours. See Commercial Support or get in touch. Community support through GitHub issues and discussions stays free.
LiveActivityContent<ShinyLiveActivityWidget>true</ShinyLiveActivityWidget>dotnet buildShinyLiveActivityWidgetSourceOnPushTokenChangedOnPushToStartTokenChangedShiny.Extensions.PushAddLiveActivities(o => o.ChannelName = ...)@available(..., unavailable)IsSupportedCancelConnection()AppleBleConfiguration.NotifyOnConnectionNotifyOnDisconnectionNotifyOnNotificationOpenL2CapFileServer(rootDirectory) turns a PSM into a file server that connected centrals can push to and pull from, with size limits, per-request authorization, path-traversal protection, and progress callbacks. L2CapTicketBroker (services.AddL2CapTicketBroker()) opens one listener and shares it between every bulk transfer: Reserve(label, lifetime, handler) returns a single-use ticket to hand the central over GATT, and the first channel to present its token gets the handler with the channel as a Stream - a wrong, expired or already-claimed token is refused and closed. [RequestResponseCharacteristic(Framed = true)] accepts requests and sends replies longer than one GATT operation - fragments are reassembled per central, the handler runs once with the whole message, and the reply is split to the central's MTU - with IGattCharacteristic.NotifyMessage(...) and BleServiceContext.GetMessageReassembler(...) for doing the same by hand. AddService(...) and StartAdvertising(...) wait out the peripheral manager's power-on handshake before talking to CoreBluetooth, so calling either at startup completes instead of hanging on a callback the OS never sends. Notify(...) on Apple waits out a full transmit queue and retries until CoreBluetooth accepts the value, rather than hanging on a missed ready callback or silently dropping the notification. Notify(data, cancellationToken, ...centrals) stops waiting when the token is cancelled or Bluetooth powers off, and the centrals you name are honoured on every platform - Apple used to send to every subscriber regardless, and Android sent a plain Notify(data) to nobody. Subscribers are tracked whether or not you pass a subscribe hook, so a reply notified to the central that wrote reaches it. A read, write or subscribe handler that throws is answered with a GATT error on Apple and Android, as it already was on Linux, instead of crashing the app from an async void callback. On Android Notify waits for each central's onNotificationSent before sending it the next value, so a burst is paced instead of silently losing everything after the first, and a central that enabled indications now gets indications. AdvertisementOptions now carries ServiceData, ManufacturerData, IsConnectable and IncludeTxPower, so a peripheral can put an arbitrary payload on the air (Android, Windows and Linux - Apple's startAdvertising reads only a local name and service UUIDs, and throws rather than silently dropping the rest). AdvertiseBeacon(...) is now correct on every platform: it built the packet from Guid.ToByteArray() and BitConverter.GetBytes(), both little-endian, and shipped a byte-swapped UUID, major and minor that no receiver could match - and prefixed AltBeacon's 0xBE 0xAC where iBeacon wants 0x02 0x15. Windows gained iBeacon broadcasting, which previously threw, and Linux gained advertising outright - StartAdvertising and AdvertiseBeacon were both unimplemented stubs. BlueZ inverts the model: rather than handing a payload to an API, the app exports an org.bluez.LEAdvertisement1 D-Bus object that BlueZ then calls back into the process to read, so Shiny exports one and registers its path with LEAdvertisingManager1. BlueZ's own Release() is honoured, so an adapter power-down or a bluetoothd restart clears IsAdvertising instead of leaving it stuck on. Linux hosts GATT services too - AddService used to throw - by exporting the ObjectManager / GattService1 / GattCharacteristic1 tree BlueZ reads and registering it with GattManager1. BlueZ only reports whether any central has notifications enabled and fans every value out to all of them, so on Linux Notify cannot address a single centralIBeaconDistanceEstimator - PathLossDistanceEstimator (d = 10^((txPower-rssi)/(10n)), n configurable for cluttered rooms) by default, with the Radius Networks/AltBeacon CurveFitDistanceEstimator available for parity with the wider Android ecosystem. Proximity is derived from the resulting distance against Apple's own boundaries (0.5m / 3m), all of it tunable through BeaconRangingOptionsCLMonitor + CLBeaconIdentityCondition on iOS/Mac Catalyst 18+ (the same path Shiny.Geofencing takes for geofences, cold-start replay suppression included) and CLLocationManager region monitoring below it, where the 20-region-per-app cap is enforced with a clear error instead of iOS silently dropping the excess0xFEAA, which CoreBluetooth passes through untouched. IEddystoneScanner decodes UID (namespace/instance), URL (with the full scheme and top-level-domain compression table), and TLM telemetry - battery volts, temperature, advertisement count and uptime, with "mains powered" and "no sensor fitted" reported as null rather than as zero and 128°C. Encrypted TLM is handed back intact for a caller holding the identity key; EID frames surface their raw rotating identifier. Filtering the scan on 0xFEAA is also what lets it keep running when an iOS app is backgroundedRegionExitTimeout (30s by default) without a matching advertisement is treated as exited. The pre-revival module got the entry side wrong in a way that mattered: it seeded the first sighting as already inside and then tested for a change, so a region entered from cold never raised an event at all. It also crashed with a duplicate key when a region was re-registered, rejected Major = 0 as invalid, accepted any manufacturer payload starting 0x02 0x15 regardless of who sent it, and ran its Android foreground service as location where Android 14 requires connectedDevice for a BLE scan. All fixed, and the state machine is covered by tests against a fake clockStartIBeacon(uuid, major, minor) works on iOS, Mac Catalyst, macOS, Android, Windows and Linux; StartEddystoneUid(...)/StartEddystoneUrl(...) on Android, Windows and Linux. Apple cannot broadcast Eddystone at all (no service data in startAdvertising) and stops emitting a decodable beacon once the app is backgrounded - both reported as a PlatformNotSupportedException naming the actual limit rather than appearing to workAccessState.NotSupported through the same channel every other capability uses instead of throwing at startup. Linux does all four through Shiny.BluetoothLE.Linux and Shiny.BluetoothLE.Hosting.Linux. Blazor reads both formats where the browser exposes requestLEScan. tvOS has no CLBeacon binding whatsoever, so it has no beacons targetWifiCapabilitiesWifiNotSupportedExceptionIWifiManager - Scan(ct) returns one WifiNetwork per BSSID with SSID, security scheme, dBm + 0-100 signal, frequency, band and channel. Connect(new WifiConnectionRequest(ssid) { Passphrase = ... }) joins and waits for DHCP rather than returning on association, and with Remember left on also persists the network for later (a WifiNetworkSuggestion on Android 11+, an ordinary profile elsewhere). await GetCurrentNetwork(ct) reports the joined network - SSID, BSSID, security, signal, plus every IP, DNS resolver, gateway and mask - and Changed fires with the new WifiNetworkInfo? (null when Wi-Fi drops), de-duplicated so the chatty native watchers behind it do not leak through, delivering the current network once on subscribe. Reading the SSID is asynchronous because both phone platforms stopped answering synchronously: iOS 14 replaced CNCopyCurrentNetworkInfo with NEHotspotNetwork.fetchCurrent, and Android 12 redacts the SSID and BSSID out of every pull-style read no matter what permissions are held, handing them out only through a NetworkCallback registered with FLAG_INCLUDE_LOCATION_INFOGetKnownNetworks() lists what the device has saved as KnownWifiNetwork (opaque platform Id, SSID, security, hidden), Forget(id) deletes one, and Connect(id) rejoins one without handing the passphrase over again. The scope differs and the API says which: iOS/Mac Catalyst and Android only ever disclose your own app's entries (NEHotspotConfigurationManager.getConfiguredSSIDs, network suggestions), while Windows, macOS and Linux hand back every profile on the machine. Connect(id) works on Windows, macOS, Linux and Android below API 29 - iOS and modern Android treat a saved network as a standing hint the OS acts on, with no call to force it. The Id is the platform's own handle: a NetworkManager connection UUID on Linux, a numeric network id on legacy Android, the SSID everywhere elseIWifiHotspot - Start(...) returns an IHotspotSession carrying the SSID and passphrase actually in use; dispose it to bring the access point down. GetClients() lists joined devices with MAC and address on Windows and Linux. Android raises a local-only hotspot (clients reach the device, not the internet) and picks the SSID/passphrase itself, Windows shares the machine's internet connection, Linux runs NetworkManager AP mode with DHCP + NAT, and iOS/macOS have no hotspot API at allWifiManager + ConnectivityManager on Android (specifier-based joins from API 29, legacy WifiConfiguration below), NEHotspotConfiguration + CaptiveNetwork on iOS/Mac Catalyst, CoreWLAN on macOS, WiFiAdapter + NetworkOperatorTetheringManager + Radio on Windows, and NetworkManager over D-Bus on Linux (separate Shiny.Net.Wifi.Linux package). Saved profiles are outside WinRT entirely, so Windows reaches wlanapi.dll directly for those three calls. On Linux, Shiny.Net.Wifi.Linux supplies AddWifi/AddWifiHotspot; the base package declares them only for the platform targets, so the two never collide. The plain .NET stub (NetWifiManager) still reports IP/DNS off the wireless interface and raises Changed if you register it yourself; its Wi-Fi-specific calls throwACCESS_WIFI_STATE, CHANGE_WIFI_STATE, ACCESS_FINE_LOCATION and NEARBY_WIFI_DEVICES (API 33+); iOS needs the Hotspot Configuration and Access WiFi Information capabilities plus NSLocationWhenInUseUsageDescription; macOS needs the location usage description; Windows needs wiFiControl and radios; Linux gates the mutating calls behind polkit. A scan that comes back empty because location was refused throws WifiPermissionException instead of looking like an empty neighbourhoodVpnService, which this package does not provide
IVpnManager - SaveProfile(new VpnProfile { Name, ServerAddress, Authentication = VpnAuthentication.SharedSecret(...) / UsernamePassword(...) / Certificate(pkcs12, password) }) installs or replaces the app's one profile and shows the OS consent prompt the first time, returning AccessState.Denied if the user declines. Connect(timeout) starts the tunnel and waits until it is connected, throwing VpnConnectionException with the OS's reason (authentication rejected, server not responding, ...) if it fails or times out. Disconnect(), RemoveProfile(), GetStatus() and a de-duplicated StatusChanged event (current state delivered on subscribe) cover the rest. RemoteIdentifier, LocalIdentifier and a private-CA ServerRootCertificate are optionalVpnCapabilities; anything missing throws VpnNotSupportedException naming the limit. Apple: everything except ServerRootCertificate (a private CA has to come from a configuration profile). Android: needs API 30, and API 33 for Status - on Android 11 and 12 Connect returns once the OS accepts the start, since there is no way to read the state; the server address is always the remote identity. Linux: shared secret and username/password, no certificate authenticationNEVPNManager + NEVPNProtocolIKEv2 on iOS/Mac Catalyst/macOS (secrets go into the keychain as persistent references), VpnManager + Ikev2VpnProfile platform VPN profiles on Android (the OS's own client, not VpnService), and NetworkManager's strongSwan plugin over D-Bus on Linux (separate Shiny.Net.Vpn.Linux package, which supplies AddVpn)com.apple.developer.networking.vpn.api = allow-vpn); Android needs nothing in the manifest; Linux needs network-manager-strongswan and polkit authorization for saving and connectingIPrintService.CapabilitiesShiny.Printingwindow.print()Shiny.Printing.Rendering renders the thermal PrintDocument to a PDF with SkiaSharp, so the same receipt can go to either kind of printer. On Linux add SkiaSharp.NativeAssets.Linux to the app - SkiaSharp's plain net10.0 build only carries the macOS and Windows nativesNSLocalNetworkUsageDescription plus _pdl-datastream._tcp and _printer._tcp in NSBonjourServices on Apple platforms, or iOS blocks both the browse and the socket. A sandboxed macOS app needs com.apple.security.print to reach CUPSGamepadButton.A is the bottom face button everywhere - A on Xbox, Cross on PlayStation, B on a Nintendo pad, whose face buttons are mirrored - so "A jumps" means the same thing on every controller. IGamepad.Kind is there for drawing the right glyph, and changes nothing about reading input. Stick Y is positive upwards on every backend; Android, Linux and the browser all report it downwards natively and each one flips it, rather than leaving six inconsistent conventions in the openpad.GetState() returns the whole pad as one immutable GamepadState - buttons as a bitmask, both sticks, both triggers - which is what a render loop wants and which never blocks. ButtonChanged and AxisChanged report the transitions, which is what menus want and which saves writing edge detection. The events are derived from the same state on every platform, so they cannot disagree with it: one event per button, never a combined mask (a handler switching on e.Button would otherwise silently miss the second of two simultaneous presses)IGamepadManager - await manager.GetGamepads() starts the watch and returns what is connected; after that Connected/Disconnected keep the list current and nothing else is asynchronous. Registering the service touches no hardware - a console host that resolves the manager and never asks for a controller opens no device node, hooks no activity and subscribes to no notification centre. AxisChangeThreshold (0.02 by default) is measured against the value each axis last reported, not against the previous state, so a slow stick sweep still produces events where a per-frame comparison would produce none; a stick landing exactly on centre or full deflection always reports, because swallowing the return to zero leaves the player walking into a wallGamepadStick.WithDeadzone(0.15f) is radial and rescales what is left. Deadzoning each axis independently is the classic bug that makes a stick feel like it has eight directions: a small X is discarded while a large Y survives, and the stick snaps to the compass points. Without the rescale the value jumps from 0 straight to the deadzone the moment it leaves the dead area, and slow movement is impossibleIGamepad.Capabilities throws GamepadNotSupportedException naming the flag and the platform reason. SetVibration sets a level that holds until changed - GamepadVibration.Off stops it, and pad.Pulse(0.8f, TimeSpan.FromMilliseconds(120)) does the one-shot bump, stopping the motors even if it is cancelled. LowFrequency is the heavy left-handle weight and HighFrequency the light right-hand buzz; trigger motors are a separate claim and are never silently folded into the handlesInputDevice + VibratorManager/LightsManager/SensorManager on Android. Windows.Gaming.Input on Windows, polled, because that API has no input callback at all. evdev on Linux (in Shiny.Gamepad's net10.0 build, picked with OperatingSystem.IsLinux()) - ioctls straight to /dev/input/event* with no libevdev, force-feedback rumble, and battery and light bar read from the controller's sysfs subtree. The W3C Gamepad API in the browser (same build, picked with OperatingSystem.IsBrowser()), where the polling loop lives in JavaScript on requestAnimationFrame and only calls into .NET when something changed - a player holding a controller still costs zero interop callsAndroidGamepadManager.ConsumeEvents keeps them for a game. State is accumulated from events, so a button held while the app is backgrounded is cleared on focus loss rather than staying stuck down/dev/input/event*, which usually means the input group. Rumble on Linux additionally needs write access to the same node, and the light bar needs write access to its sysfs LED - the node is opened read-write when permitted and read-only otherwise, so a controller always works and simply reports no Vibration where the permission is missing. Windows.Gaming.Input reports every stick centred while the app is not in the foreground, which is the API's own behaviour and not something the app can opt out ofIWearableManager reports pairing and reachability as separate questions - IsConfigured (paired, companion app installed) survives the watch being switched off or out of range, while IsReachable means a live message can go now, and every known watch is listed connected or not so an absent one can still be named (on Wear OS it is found through the companion app's capability, because the connected-node list drops a watch the moment it disconnects) - sends live messages and awaits the companion's reply, shares the latest context both ways, and queues data and file transfers that survive the watch being out of range - payloads go as key/values (sent as a JSON object) or plain text, with no hand-encoding to bytes or JSON, and received messages read back with GetString() / GetValues(). Everything the watch sends reaches an IWearableDelegate, with the app woken in the background for it; a delegate's return value is the reply to the watch's message. WearableProtocol documents the wire format for Swift and Kotlin companions, and on Wear OS the same package runs inside a .NET watch app. iOS and Android; elsewhere nothing is registeredScreenRecorderCapabilities flags property; anything unavailable throws ScreenRecorderNotSupportedException naming the specific limit. The flags are read off the instance because they differ within a platform: macOS 15 gains microphone capture and loses pause (SCRecordingOutput writes the file itself and cannot be detached mid-recording) where macOS 12.3-14 has it the other way roundIScreenRecorder - await recorder.Start(new ScreenRecordingRequest { IncludeMicrophone = true, MaxWidth = 1280 }) returns an IScreenRecording that does not come back until frames are genuinely being written - so a consent dialog, a compositor picker or an Android foreground-service promotion all complete first. Pause()/Resume() close the gap in the timeline rather than leaving a frozen stretch, Stop() returns a ScreenRecordingResult with the path, duration, size and the MimeType actually produced (browsers disagree - Safari and Chrome give MP4, Firefox gives WebM), and disposing without stopping cancels and deletes the partial file. A request asking for something outside Capabilities throws before any native call, because a recording that silently came out without the microphone is worse than one that refused to startGetTargets() lists displays, windows and applications on macOS and Windows, so an app can offer its own picker. Linux and the browser hand selection to the compositor and throw here - their picker appears during Start insteadIScreenRecording.Faulted fires with a reason - the user hit Android's cast notification or the browser's "Stop sharing" bar, iOS took the screen for an incoming call, a monitor was unplugged, the encoder failed - and carries whatever was salvaged, so a recording cut short still yields a playable file rather than a silent truncation. MaxDuration stops cleanly and reports the same waystartCapture into an AVAssetWriter on iOS/tvOS/Mac Catalyst (startRecording only surrenders its file through a share sheet, which is no use to a library), ScreenCaptureKit on macOS - SCRecordingOutput on 15+ and the same AVAssetWriter below it - Windows.Graphics.Capture into a MediaStreamSource transcoded by Media Foundation on Windows, and the xdg-desktop-portal ScreenCast API driving gst-launch-1.0 or ffmpeg on Linux (separate Shiny.ScreenRecorder.Linux package, AddLinuxScreenRecorder(), so its D-Bus dependency stays out of WebAssembly apps). Blazor WebAssembly uses getDisplayMedia + MediaRecorder (in Shiny.ScreenRecorder's net10.0 build, picked with OperatingSystem.IsBrowser()) and is the only platform where pause is native and the result has no file path - there is no filesystem, so OpenRead() is the portable accessorFOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PROJECTION and RECORD_AUDIO for audio, plus a per-recording consent dialog that cannot be pre-granted - it asks for the entire screen, so Android 14+'s "a single app" picker never appears - and the recording notification carries a Stop action that ends the recording like the system's own control (reword it with IScreenRecordingNotificationDelegate); iOS needs NSMicrophoneUsageDescription and the app in the foreground; macOS needs the Screen Recording grant (and a relaunch after it is first given); Windows needs 1903+ and the graphicsCapture capability when packaged; Linux needs a portal plus GStreamer or FFmpeg, all probed at runtime so a machine missing them reports None rather than failing at record time. Windows has no audio at all - Windows.Graphics.Capture captures pixels onlyIBiometrics: LocalAuthentication on iOS, Mac Catalyst and macOS (Touch ID, Face ID, Optic ID - and a paired Apple Watch on macOS when the passcode is allowed), AndroidX BiometricPrompt on Android (fingerprint, face, iris; Strong / Weak class) and Windows Hello (UserConsentVerifier, parented to the app window). GetAvailability() reports a BiometricStatus (Available, NotEnrolled, LockedOut, PasscodeNotSet, ...), the BiometricType hardware present and whether a passcode is set, with no UI; await biometrics.Authenticate(new BiometricRequest { Reason = "Unlock your vault", AllowDeviceCredential = true }) returns a BiometricResult (Success, Cancelled, Fallback, LockedOut, ...) instead of throwing, and a cancelled token dismisses the prompt. Windows Hello always offers its PIN and never says which method was used. The result is a local yes/no - gate UI with it, don't treat it as a credential. iOS needs NSFaceIDUsageDescription in Info.plist. Linux, Blazor and plain .NET report NotSupported. Register with services.AddBiometrics() (docs)INfcManager. NDEF only, on purpose - payment cards, passports, MIFARE Classic and raw tag commands are out of scope. Android uses reader mode on the current activity (re-armed across rotation and activity changes), iOS a CoreNFC NFCNDEFReaderSession (the scan sheet stays up for the whole scan; no tag UID), Windows, macOS and Linux a PC/SC contactless reader such as an ACR122U (winscard.dll, PCSC.framework, pcsc-lite), where the library speaks NDEF itself to NFC Forum Type 2 (NTAG, Ultralight) and Type 4 tags, and Blazor WebAssembly Web NFC (Chrome on Android). A scan is an IAsyncEnumerable<INfcTag> whose session lives exactly as long as the await foreach: await foreach (var tag in nfc.Scan(ct: ct)) { var msg = await tag.ReadNdef(); await tag.WriteNdef(new NdefMessage(NdefRecord.CreateUri("https://shinylib.net"))); }, or the one-tap ReadOne() / WriteOne(message). NdefMessage / NdefRecord encode and parse NDEF on every platform. Capabilities reports what the platform can do; MakeReadOnly() locks a tag permanently. Android needs android.permission.NFC; iOS needs NFCReaderUsageDescription and the com.apple.developer.nfc.readersession.formats entitlement (TAG); Linux needs pcscd; a sandboxed macOS app needs com.apple.security.smartcard. Register with services.AddNfc() (docs)IAgeSignals over Apple Declared Age Range (iOS / macOS 26+, through a bundled Swift shim - the framework is Swift-only) and Google Play Age Signals (Android, the 0.0.4 AAR bound inside the package): var signal = await ageSignals.RequestAgeRange(13, 16, 18) returns a Status (Shared, Declined, Play's VerificationRequired, NotSupported), the bounds, and an AgeRangeSource ordered weakest first (SelfDeclared < GuardianDeclared < Confirmed < IdVerified) so an app can require a minimum. signal.IsAtLeast(16) and IsAdult return bool? - null when the store can't tell - so "unknown" is never read as "no". GetEligibility() reports Apple's isEligibleForAgeFeatures and 26.4 regulatory flags with no UI; ShowSignificantUpdate(...) presents Apple's significant-update acknowledgment (iOS 26.4+), and Play's guardian approval of significant changes comes back on AgeSignal.SignificantChange with the InstallId used for revocations. The age gates are Apple's - Play's bands come from Play Console. Nothing is cached. Apple needs the com.apple.developer.declared-age-range entitlement - without it the call never returns. Mac Catalyst, Windows, Linux and Blazor report NotSupported. Register with services.AddAgeSignals() (docs)com.google.android.gms.games.APP_ID