<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Shiny.NET | Blog</title><description/><link>https://www.shinylib.net/</link><language>en</language><item><title>Introducing Shiny.AiConversation — AI Conversation</title><link>https://www.shinylib.net/blog/2026/05/ai-conversation/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/05/ai-conversation/</guid><pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;a href=&quot;https://www.nuget.org/packages/Shiny.AiConversation&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://img.shields.io/nuget/v/Shiny.AiConversation?style=for-the-badge&amp;#x26;logo=nuget&amp;#x26;label=Shiny.AiConversation&quot; alt=&quot;NuGet package Shiny.AiConversation&quot;&gt;&lt;/a&gt;
&lt;aside aria-label=&quot;Early Beta&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Early Beta&lt;/p&gt;&lt;div&gt;&lt;p&gt;This library is in active development. APIs may change, and some features are still being stabilized. Feedback and bug reports are very welcome, but don’t ship it to production just yet.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;Building an AI-powered app today means stitching together a chat client, speech recognition, text-to-speech, audio playback, message persistence, and state management — across platforms, with proper lifecycle handling.  That’s a lot of plumbing before you write your first prompt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shiny.AiConversation&lt;/strong&gt; wraps all of that into a single &lt;code dir=&quot;auto&quot;&gt;IAiConversationService&lt;/code&gt; interface.  Text chat, voice chat, hands-free wake word activation, configurable audio feedback, and persistent chat history — registered with one DI call, consumed through one service.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every AI chat app ends up building the same infrastructure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An authenticated chat client that handles token refresh&lt;/li&gt;
&lt;li&gt;Speech-to-text so users can talk instead of type&lt;/li&gt;
&lt;li&gt;Text-to-speech so the AI can respond out loud&lt;/li&gt;
&lt;li&gt;Sound effects for state transitions (thinking, responding, error)&lt;/li&gt;
&lt;li&gt;A wake word listener for hands-free mode&lt;/li&gt;
&lt;li&gt;Message persistence for chat history&lt;/li&gt;
&lt;li&gt;State management so the UI knows what’s happening&lt;/li&gt;
&lt;li&gt;Thread safety so nothing blows up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these is a separate library, a separate abstraction, and a separate set of platform quirks.  You spend weeks on infrastructure before you ship a single feature.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-solution&quot;&gt;The Solution&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Register your chat client in DI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddChatClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OpenAIClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;your-api-key&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;GetChatClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;AsIChatClient&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddShinyAiConversation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.&lt;/span&gt;&lt;span&gt;SetMessageStore&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyMessageStore&lt;/span&gt;&lt;span&gt;&gt;(); &lt;/span&gt;&lt;span&gt;// optional&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it.  The service registers &lt;code dir=&quot;auto&quot;&gt;IAiConversationService&lt;/code&gt; with all the wiring — speech services from &lt;a href=&quot;https://www.shinylib.net/speech/&quot;&gt;Shiny.Speech&lt;/a&gt;, chat completions from &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.Extensions.AI&quot;&gt;Microsoft.Extensions.AI&lt;/a&gt;, audio playback, time provider, and optional message persistence.  The default &lt;code dir=&quot;auto&quot;&gt;IChatClientProvider&lt;/code&gt; resolves &lt;code dir=&quot;auto&quot;&gt;IChatClient&lt;/code&gt; straight from DI, so for most apps you just register your chat client and go.  For advanced scenarios (on-demand auth, token refresh), you can still implement &lt;code dir=&quot;auto&quot;&gt;IChatClientProvider&lt;/code&gt; directly.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-you-can-do&quot;&gt;What You Can Do&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;text-chat&quot;&gt;Text Chat&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The simplest path.  Send a message, get a streaming response:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.AiResponded &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (response.Response.Text &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; { } &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Console.&lt;/span&gt;&lt;span&gt;WriteLine&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;$&quot;AI: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; aiService.&lt;/span&gt;&lt;span&gt;TalkTo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;What is .NET MAUI?&quot;&lt;/span&gt;&lt;span&gt;, cancellationToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The service handles the full lifecycle — acquires the chat client, prepends system prompts, streams the response, stores both messages if a message store is configured, fires the event, and manages state transitions throughout.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;voice-chat-push-to-talk&quot;&gt;Voice Chat (Push-to-Talk)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;One method call captures speech and sends it to the AI:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; aiService.&lt;/span&gt;&lt;span&gt;ListenAndTalk&lt;/span&gt;&lt;span&gt;(cancellationToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The service activates speech-to-text, waits for the user to stop speaking, sends the transcribed text through &lt;code dir=&quot;auto&quot;&gt;TalkTo()&lt;/code&gt;, and optionally reads the response aloud via text-to-speech.  If the AI responds with a question, the service automatically keeps listening for the user’s reply — creating a natural back-and-forth conversation without requiring another button press.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;hands-free-wake-word&quot;&gt;Hands-Free Wake Word&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is the “Hey Siri” experience:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; aiService.&lt;/span&gt;&lt;span&gt;StartWakeWord&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Hey Copilot&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The service enters a continuous loop: listen for the wake phrase, capture the utterance that follows, send it to the AI, loop back.  If the AI asks a follow-up question, the loop skips wake word detection and listens directly for the user’s reply.  The user never touches the screen.  Call &lt;code dir=&quot;auto&quot;&gt;StopWakeWord()&lt;/code&gt; when you’re done.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;four-acknowledgement-modes&quot;&gt;Four Acknowledgement Modes&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Control how the AI delivers responses:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Mode&lt;/th&gt;&lt;th&gt;What Happens&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;None&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Silent — text only, delivered via the &lt;code dir=&quot;auto&quot;&gt;AiResponded&lt;/code&gt; event&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;AudioBlip&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Short sound effects at each state transition&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;LessWordy&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Text-to-speech with a “be concise” system prompt&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Full&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Full text-to-speech of the complete response&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sound effects are driven by string file names and a &lt;code dir=&quot;auto&quot;&gt;SoundResolver&lt;/code&gt; callback — the library stays platform-agnostic while you provide the stream:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.SoundResolver &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; FileSystem.&lt;/span&gt;&lt;span&gt;OpenAppPackageFileAsync&lt;/span&gt;&lt;span&gt;(name);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.ThinkSound &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;think.mp3&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.OkSound &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;ok.mp3&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;voice-interruption&quot;&gt;Voice Interruption&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;During TTS playback, the service listens for voice interruptions.  Say a “quiet word” like “stop” or “cancel” and TTS is silenced immediately, breaking out of the conversation.  Say anything else and TTS stops, but your new utterance is sent to the AI as the next message — the conversation continues seamlessly.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Default quiet words: cancel, quiet, shut up, stop, nevermind, never mind, hush&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Customize or disable:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.QuietWords &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;stop&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;cancel&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.QuietWords &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// disable interruption&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Quiet words only trigger when they’re the user’s &lt;em&gt;entire&lt;/em&gt; utterance.  “Cancel this appointment” won’t interrupt — it’ll be treated as a new message to the AI.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;chat-history-with-ai-self-lookup&quot;&gt;Chat History with AI Self-Lookup&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Register an &lt;code dir=&quot;auto&quot;&gt;IMessageStore&lt;/code&gt; and every message is automatically persisted.  But the interesting part is the &lt;strong&gt;AI chat lookup tool&lt;/strong&gt; — it’s an &lt;code dir=&quot;auto&quot;&gt;AITool&lt;/code&gt; that lets the AI search its own conversation history:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“What did we talk about yesterday?”&lt;/em&gt;
&lt;em&gt;“Find the recipe you gave me last week.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The tool is registered automatically when you call &lt;code dir=&quot;auto&quot;&gt;SetMessageStore()&lt;/code&gt;.  The AI gets search parameters (text, date range, limit) and queries your store directly.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;observable-state&quot;&gt;Observable State&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The service exposes its current state and fires events:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aiService.StatusChanged &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// state: Idle, Listening, Thinking, Responding&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UpdateUI&lt;/span&gt;&lt;span&gt;(state);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is what powers the “Aura” visualization in our sample app — a pulsing orb that changes color based on what the AI is doing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;bring-your-own-backend&quot;&gt;Bring Your Own Backend&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The library doesn’t care which AI you use.  By default, it resolves &lt;code dir=&quot;auto&quot;&gt;IChatClient&lt;/code&gt; from DI — just register one and you’re done.  For advanced auth scenarios, implement &lt;code dir=&quot;auto&quot;&gt;IChatClientProvider&lt;/code&gt; to return any &lt;code dir=&quot;auto&quot;&gt;IChatClient&lt;/code&gt; from &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OpenAI&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;new OpenAIClient(apiKey).GetChatClient(&quot;gpt-4o&quot;).AsIChatClient()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt; — OAuth device code flow with Copilot API token exchange&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure OpenAI&lt;/strong&gt; — Managed identity or API key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ollama&lt;/strong&gt; — Local model, no auth needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anything else&lt;/strong&gt; — If it implements &lt;code dir=&quot;auto&quot;&gt;IChatClient&lt;/code&gt;, it works&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sample apps include a complete GitHub Copilot implementation with device code flow, token caching, automatic re-authentication, and the custom HTTP headers the Copilot API requires.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;cross-platform-from-day-one&quot;&gt;Cross-Platform from Day One&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The library targets plain &lt;code dir=&quot;auto&quot;&gt;net10.0&lt;/code&gt; — no MAUI dependency in the library itself.  &lt;a href=&quot;https://www.shinylib.net/speech/&quot;&gt;Shiny.Speech&lt;/a&gt; handles the platform abstraction for speech and audio, so the same &lt;code dir=&quot;auto&quot;&gt;IAiConversationService&lt;/code&gt; works on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAUI&lt;/strong&gt; — Android, iOS, Windows, Mac Catalyst&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blazor&lt;/strong&gt; — Server-side and WebAssembly (speech via Web Audio API)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We ship two sample apps that prove it: a full &lt;a href=&quot;https://github.com/shinyorg/aiconversation/tree/main/samples/Sample&quot;&gt;MAUI sample&lt;/a&gt; with chat, settings, and an animated aura visualization, plus a &lt;a href=&quot;https://github.com/shinyorg/aiconversation/tree/main/samples/Sample.Blazor&quot;&gt;Blazor Server sample&lt;/a&gt; with the same features translated to Razor components and CSS animations.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;aot-compatible&quot;&gt;AOT Compatible&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The library is built with &lt;code dir=&quot;auto&quot;&gt;IsAotCompatible=true&lt;/code&gt;.  Generic type parameters on &lt;code dir=&quot;auto&quot;&gt;SetChatClientProvider&amp;#x3C;T&gt;()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;SetMessageStore&amp;#x3C;T&gt;()&lt;/code&gt; carry &lt;code dir=&quot;auto&quot;&gt;[DynamicallyAccessedMembers]&lt;/code&gt; attributes so the trimmer knows what to keep.  No reflection surprises at runtime.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.AiConversation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.shinylib.net/aiconversation/&quot;&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shinyorg/aiconversation&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shinyorg/aiconversation/tree/main/samples/Sample&quot;&gt;MAUI Sample&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shinyorg/aiconversation/tree/main/samples/Sample.Blazor&quot;&gt;Blazor Sample&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The library is MIT licensed and open source.  We’d love to hear what you build with it.&lt;/p&gt;</content:encoded><category>Release</category><category>AI</category></item><item><title>The Feedback Service — One Hook to Rule Them All</title><link>https://www.shinylib.net/blog/2026/05/feedback-service/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/05/feedback-service/</guid><pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;a href=&quot;https://www.nuget.org/packages/Shiny.Maui.Controls&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://img.shields.io/nuget/v/Shiny.Maui.Controls?style=for-the-badge&amp;#x26;logo=nuget&amp;#x26;label=Shiny.Maui.Controls&quot; alt=&quot;NuGet package Shiny.Maui.Controls&quot;&gt;&lt;/a&gt;
&lt;p&gt;Every tap, swipe, and keystroke in your app is an opportunity.  An opportunity to confirm the user’s action, guide their attention, or add a layer of polish that separates “functional” from “delightful.”  Most apps handle this with scattered &lt;code dir=&quot;auto&quot;&gt;HapticFeedback.Default.Perform()&lt;/code&gt; calls sprinkled across code-behind files.  It works — until you want text-to-speech for accessibility, sound effects for a kiosk app, analytics for product telemetry, or different feedback for different controls.  Then you’re threading conditional logic through every view in your app.&lt;/p&gt;
&lt;p&gt;Shiny Controls v1.0 ships with &lt;code dir=&quot;auto&quot;&gt;IFeedbackService&lt;/code&gt; — a single injectable service that every interactive control in the library already calls.  You implement it once.  Every control uses it automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every Shiny control that supports feedback has a &lt;code dir=&quot;auto&quot;&gt;UseFeedback&lt;/code&gt; property (default: &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt;).  When a user interaction occurs — a message sent, a pin digit entered, a panel opened — the control calls &lt;code dir=&quot;auto&quot;&gt;IFeedbackService.OnRequested()&lt;/code&gt; with three things:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IFeedbackService&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OnRequested&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;control&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eventName&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;control&lt;/code&gt;&lt;/strong&gt; — the actual control instance, not a &lt;code dir=&quot;auto&quot;&gt;Type&lt;/code&gt;.  Pattern match directly: &lt;code dir=&quot;auto&quot;&gt;control is ChatView&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;control is SecurityPin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;eventName&lt;/code&gt;&lt;/strong&gt; — what happened: &lt;code dir=&quot;auto&quot;&gt;&quot;MessageReceived&quot;&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;&quot;DigitEntered&quot;&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;&quot;Opened&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;args&lt;/code&gt;&lt;/strong&gt; — contextual data.  For &lt;code dir=&quot;auto&quot;&gt;ChatView&lt;/code&gt;, this is the full &lt;code dir=&quot;auto&quot;&gt;ChatMessage&lt;/code&gt; object.  For standard MAUI controls, it’s the native &lt;code dir=&quot;auto&quot;&gt;EventArgs&lt;/code&gt;.  For &lt;code dir=&quot;auto&quot;&gt;SecurityPin&lt;/code&gt; completion, it’s &lt;code dir=&quot;auto&quot;&gt;&quot;LongPress&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The default &lt;code dir=&quot;auto&quot;&gt;HapticFeedbackService&lt;/code&gt; does what you’d expect — click haptic for most events, long press haptic for completion events.  But the real power is in replacing it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;custom-feedback-tts--sound-effects&quot;&gt;Custom Feedback: TTS + Sound Effects&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s a real example from our sample app.  One service, three behaviors — haptic, text-to-speech for incoming chat messages, and audio cues for PIN entry:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyCustomFeedbackService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ITextToSpeechService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;textToSpeech&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IAudioManager&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;audioManager&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;HapticFeedbackService&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OnRequested&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;control&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eventName&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// haptic first — always&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OnRequested&lt;/span&gt;&lt;span&gt;(control, eventName, args);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// speak incoming chat messages aloud&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (control &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatView&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; args &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatMessage&lt;/span&gt;&lt;span&gt; { IsFromMe: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;msg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; textToSpeech.&lt;/span&gt;&lt;span&gt;SpeakAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;$&quot;Message from &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;msg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SenderId&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;msg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Text&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// click and success sounds for PIN entry&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (control &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityPin&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sound&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; eventName.&lt;/span&gt;&lt;span&gt;Equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;completed&quot;&lt;/span&gt;&lt;span&gt;, StringComparison.OrdinalIgnoreCase)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;pin_success.wav&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;pin_click.wav&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; FileSystem.&lt;/span&gt;&lt;span&gt;OpenAppPackageFileAsync&lt;/span&gt;&lt;span&gt;(sound);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;audioManager.&lt;/span&gt;&lt;span&gt;CreatePlayer&lt;/span&gt;&lt;span&gt;(raw).&lt;/span&gt;&lt;span&gt;Play&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Register it in one line:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.&lt;/span&gt;&lt;span&gt;UseShinyControls&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cfg.&lt;/span&gt;&lt;span&gt;SetCustomFeedback&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyCustomFeedbackService&lt;/span&gt;&lt;span&gt;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Because &lt;code dir=&quot;auto&quot;&gt;control&lt;/code&gt; is the live instance and &lt;code dir=&quot;auto&quot;&gt;args&lt;/code&gt; carries typed data, you can make nuanced decisions without parsing strings.  The &lt;code dir=&quot;auto&quot;&gt;ChatMessage&lt;/code&gt; gives you sender, timestamp, text, and image URL.  The &lt;code dir=&quot;auto&quot;&gt;SecurityPin&lt;/code&gt; instance gives you its current value and length.  Cast, match, and go.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;pluggable-maui-control-hooks&quot;&gt;Pluggable MAUI Control Hooks&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Shiny’s own controls call &lt;code dir=&quot;auto&quot;&gt;IFeedbackService&lt;/code&gt; internally.  But what about standard MAUI controls — &lt;code dir=&quot;auto&quot;&gt;Button&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Slider&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Entry&lt;/code&gt;?  The &lt;code dir=&quot;auto&quot;&gt;MauiControlFeedbackBuilder&lt;/code&gt; hooks them in automatically, with an AOT-compatible, fully pluggable design:&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;all-defaults&quot;&gt;All defaults&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cfg.&lt;/span&gt;&lt;span&gt;AddDefaultMauiControlFeedback&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This registers hooks for 12 standard MAUI controls — &lt;code dir=&quot;auto&quot;&gt;Button.Clicked&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Entry.TextChanged&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Slider.ValueChanged&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Switch.Toggled&lt;/code&gt;, and more.  Each hook passes the control instance as &lt;code dir=&quot;auto&quot;&gt;control&lt;/code&gt; and the native event args as &lt;code dir=&quot;auto&quot;&gt;args&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;defaults--your-own&quot;&gt;Defaults + your own&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cfg.&lt;/span&gt;&lt;span&gt;AddDefaultMauiControlFeedback&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;Hook&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyCustomControl&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;nameof&lt;/span&gt;&lt;span&gt;(MyCustomControl.Tapped),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.Tapped &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; h,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.Tapped &lt;/span&gt;&lt;span&gt;-=&lt;/span&gt;&lt;span&gt; h);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;only-what-you-need&quot;&gt;Only what you need&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cfg.&lt;/span&gt;&lt;span&gt;AddMauiControlFeedback&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;Hook&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Button&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;nameof&lt;/span&gt;&lt;span&gt;(Button.Clicked),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;btn&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; btn.Clicked &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; h,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;btn&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; btn.Clicked &lt;/span&gt;&lt;span&gt;-=&lt;/span&gt;&lt;span&gt; h);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;Hook&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Slider&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ValueChangedEventArgs&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;nameof&lt;/span&gt;&lt;span&gt;(Slider.ValueChanged),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; s.ValueChanged &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; h,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; s.ValueChanged &lt;/span&gt;&lt;span&gt;-=&lt;/span&gt;&lt;span&gt; h);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Two overloads cover every case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Hook&amp;#x3C;TControl&gt;(eventName, subscribe, unsubscribe)&lt;/code&gt; for plain &lt;code dir=&quot;auto&quot;&gt;EventHandler&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Hook&amp;#x3C;TControl, TEventArgs&gt;(eventName, subscribe, unsubscribe)&lt;/code&gt; for typed &lt;code dir=&quot;auto&quot;&gt;EventHandler&amp;#x3C;TEventArgs&gt;&lt;/code&gt; events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under the hood, each hook uses a &lt;code dir=&quot;auto&quot;&gt;ConditionalWeakTable&lt;/code&gt; to track handlers per control instance — no leaks, no dictionaries to manage, proper unsubscription when controls leave the visual tree.  Zero reflection, fully AOT-safe.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-ships-built-in&quot;&gt;What Ships Built-In&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every Shiny control fires feedback through this system.  Here’s the full event catalog:&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Control&lt;/th&gt;&lt;th&gt;Events&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ChatView&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MessageSent&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;MessageReceived&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;MessageTapped&lt;/code&gt; (all pass &lt;code dir=&quot;auto&quot;&gt;ChatMessage&lt;/code&gt;), &lt;code dir=&quot;auto&quot;&gt;AttachImage&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SecurityPin&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;DigitEntered&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Completed&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;FloatingPanel&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Opened&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Closed&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DetentChanged&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ImageViewer&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Opened&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Closed&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DoubleTapped&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ImageEditor&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;ToolModeChanged&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Undo&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Redo&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Rotate&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Reset&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;CropApplied&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Saved&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fab / FabMenu&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Clicked&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Toggled&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Scheduler&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;DaySelected&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;EventSelected&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;TimeSlotSelected&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TableView Cells&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Tapped&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Toast&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Show&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Any control’s feedback can be suppressed per-instance with &lt;code dir=&quot;auto&quot;&gt;UseFeedback=&quot;False&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-design-philosophy&quot;&gt;The Design Philosophy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most feedback systems are either too simple (a global haptic toggle) or too complex (per-control event subscriptions scattered across your app).  &lt;code dir=&quot;auto&quot;&gt;IFeedbackService&lt;/code&gt; sits in the sweet spot:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;One service, all controls.&lt;/strong&gt;  Implement once, every control calls it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance, not type.&lt;/strong&gt;  You get the actual control, not &lt;code dir=&quot;auto&quot;&gt;typeof(Button)&lt;/code&gt;.  Inspect properties, check state, make decisions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typed args, not strings.&lt;/strong&gt;  &lt;code dir=&quot;auto&quot;&gt;ChatMessage&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;ValueChangedEventArgs&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;ToggledEventArgs&lt;/code&gt; — not &lt;code dir=&quot;auto&quot;&gt;&quot;the message text&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pluggable hooks, not hardcoded events.&lt;/strong&gt;  Add your own controls to the system with three lambdas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AOT-safe.&lt;/strong&gt;  No reflection, no expressions, no &lt;code dir=&quot;auto&quot;&gt;Delegate.CreateDelegate&lt;/code&gt;.  Just generics and delegates.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Whether you’re building an accessible app that speaks every incoming message, a kiosk that plays sound effects, or just want consistent haptic feedback across your entire UI — &lt;code dir=&quot;auto&quot;&gt;IFeedbackService&lt;/code&gt; is one implementation away.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href=&quot;https://www.shinylib.net/controls/feedback/&quot;&gt;full documentation&lt;/a&gt; and the sample app for a working demo with TTS and audio integration.&lt;/p&gt;</content:encoded><category>Controls</category><category>Release</category></item><item><title>DocumentDb AI Tools — Give Your LLM a Database</title><link>https://www.shinylib.net/blog/2026/04/documentdb-ai-tools/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/04/documentdb-ai-tools/</guid><description>Shiny.DocumentDb.Extensions.AI turns your document store into a set of LLM-callable tools. Register types, set capabilities, and let the agent query, insert, update, and delete documents through natural language.

</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You have a document store with customers, orders, and products. Now you want an LLM agent to answer questions about that data — or even modify it — without writing custom glue code for every operation. &lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.Extensions.AI&lt;/code&gt; makes that a one-time setup.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.Extensions.AI&lt;/code&gt; package generates &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt; tool functions from your registered document types. Each type can expose up to seven operations: get by ID, query with structured filters, count, aggregate (sum/min/max/avg), insert, update, and delete.&lt;/p&gt;
&lt;p&gt;You control everything: which types are visible to the LLM, which operations are allowed, which fields are exposed, and how many results a single query can return.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;setup-in-30-seconds&quot;&gt;Setup in 30 Seconds&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;jsonContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AppJsonContext&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JsonSerializerOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;PropertyNamingPolicy &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; JsonNamingPolicy.CamelCase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.DatabaseProvider &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDatabaseProvider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.JsonSerializerOptions &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; jsonContext.Options;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddDocumentStoreAITools&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tools&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tools.&lt;/span&gt;&lt;span&gt;AddType&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jsonContext.Customer,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;capabilities&lt;/span&gt;&lt;span&gt;: DocumentAICapabilities.All,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;configure&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Customer records&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Property&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.Status, &lt;/span&gt;&lt;span&gt;&quot;Active, Inactive, or Suspended&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;IgnoreProperties&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.PasswordHash)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MaxPageSize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tools.&lt;/span&gt;&lt;span&gt;AddType&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jsonContext.Order,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;capabilities&lt;/span&gt;&lt;span&gt;: DocumentAICapabilities.ReadOnly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That registers 11 tools: 7 for Customer (full CRUD) and 4 for Order (read-only). Types not registered are invisible to the LLM.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;structured-filters&quot;&gt;Structured Filters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The query, count, and aggregate tools accept a structured filter that supports boolean combinators — &lt;code dir=&quot;auto&quot;&gt;and&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;or&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;not&lt;/code&gt; — and leaf comparisons with operators like &lt;code dir=&quot;auto&quot;&gt;eq&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gt&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;contains&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;startsWith&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;in&lt;/code&gt;. The library translates these JSON filter objects into LINQ expressions against the document store, so they work across all providers (SQLite, MySQL, SQL Server, PostgreSQL).&lt;/p&gt;
&lt;p&gt;When an LLM asks “show me customers older than 30 in Portland”, it constructs:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;and&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;field&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;age&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;op&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;gt&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;field&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;city&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;op&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;eq&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Portland&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This becomes &lt;code dir=&quot;auto&quot;&gt;store.Query&amp;#x3C;Customer&gt;().Where(c =&gt; c.Age &gt; 30 &amp;#x26;&amp;#x26; c.City == &quot;Portland&quot;)&lt;/code&gt; under the hood.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;field-visibility&quot;&gt;Field Visibility&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Not every field should be visible to an LLM agent. Use &lt;code dir=&quot;auto&quot;&gt;AllowProperties&lt;/code&gt; to create an allowlist or &lt;code dir=&quot;auto&quot;&gt;IgnoreProperties&lt;/code&gt; to hide specific fields:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Only expose these fields&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;b.&lt;/span&gt;&lt;span&gt;AllowProperties&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.Id, &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.Name, &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.Email);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Or hide sensitive fields&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;b.&lt;/span&gt;&lt;span&gt;IgnoreProperties&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.PasswordHash, &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.InternalNotes);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Hidden fields don’t appear in the tool’s JSON schema, so the LLM doesn’t know they exist and can’t filter on them.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;capability-flags&quot;&gt;Capability Flags&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;DocumentAICapabilities&lt;/code&gt; flags enum gives you precise control:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Read-only: get, query, count, aggregate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tools.&lt;/span&gt;&lt;span&gt;AddType&lt;/span&gt;&lt;span&gt;(jsonContext.AuditLog, &lt;/span&gt;&lt;span&gt;capabilities&lt;/span&gt;&lt;span&gt;: DocumentAICapabilities.ReadOnly);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Full CRUD&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tools.&lt;/span&gt;&lt;span&gt;AddType&lt;/span&gt;&lt;span&gt;(jsonContext.Customer, &lt;/span&gt;&lt;span&gt;capabilities&lt;/span&gt;&lt;span&gt;: DocumentAICapabilities.All);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Just query and count&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tools.&lt;/span&gt;&lt;span&gt;AddType&lt;/span&gt;&lt;span&gt;(jsonContext.Config, &lt;/span&gt;&lt;span&gt;capabilities&lt;/span&gt;&lt;span&gt;: DocumentAICapabilities.Query &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; DocumentAICapabilities.Count);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;aot-safe&quot;&gt;AOT-Safe&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;All tool schemas and serialization use &lt;code dir=&quot;auto&quot;&gt;JsonTypeInfo&amp;#x3C;T&gt;&lt;/code&gt; from your source-generated JSON context. No reflection at runtime. The filter translator builds expression trees programmatically — it never calls &lt;code dir=&quot;auto&quot;&gt;Expression.Compile()&lt;/code&gt; — so the existing JSON-extract SQL translator handles AOT-safe code generation.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;try-the-copilot-sample&quot;&gt;Try the Copilot Sample&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The repository includes a &lt;code dir=&quot;auto&quot;&gt;Sample.CopilotConsole&lt;/code&gt; project that authenticates with GitHub Copilot and starts an interactive chat session. The LLM can query customers, create orders, compute aggregates — all through the registered AI tools. Try it:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;samples/Sample.CopilotConsole&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Then ask things like “How many customers do we have?”, “Show me all shipped orders”, or “Add a customer named Eve, age 28”.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Check the &lt;a href=&quot;https://www.shinylib.net/documentdb/ai-tools/&quot;&gt;AI Tools documentation&lt;/a&gt; for the full API reference and the &lt;a href=&quot;https://www.shinylib.net/documentdb/release-notes/&quot;&gt;release notes&lt;/a&gt; for the complete v4.0 changelog.&lt;/p&gt;</content:encoded><category>documentdb</category><category>AI</category><category>release</category></item><item><title>Turn Any Interface Into an AI Tool — Shiny DI 3.0</title><link>https://www.shinylib.net/blog/2026/04/di-ai-tools/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/04/di-ai-tools/</guid><pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;What if every service interface you already have could become an AI tool with a single attribute? Shiny Extensions DI 3.0 makes that happen — no adapter classes, no hand-rolled schemas, no registration boilerplate. Mark your interface with &lt;code dir=&quot;auto&quot;&gt;[Tool]&lt;/code&gt;, add &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; to the methods that matter, and the source generator handles the rest.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You’ve built your services. Clean interfaces, proper DI registration, everything wired up. Now someone asks you to expose a few of those operations as AI tools for an LLM agent. Suddenly you’re writing &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; subclasses by hand — one per operation — each with a constructor that takes the service, a metadata property with hand-written parameter schemas, and an &lt;code dir=&quot;auto&quot;&gt;InvokeCoreAsync&lt;/code&gt; override that extracts arguments from a dictionary and forwards them to your service method.&lt;/p&gt;
&lt;p&gt;For one or two tools, it’s fine. For ten or twenty, it’s tedious. And every time you change a method signature, you have to remember to update the corresponding tool class. The schema drifts, the argument parsing breaks, and the bugs only show up when the LLM calls the tool at runtime.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-solution-tool--description&quot;&gt;The Solution: &lt;code dir=&quot;auto&quot;&gt;[Tool]&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Tool&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Manages customer orders&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IOrderService&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Places a new order for a customer&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;OrderResult&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;PlaceOrderAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;The customer identifier&quot;&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;Guid&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;customerId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;The product SKU&quot;&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sku&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Number of units to order&quot;&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;quantity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Cancels an existing order&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CancelOrderAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;The order to cancel&quot;&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;Guid&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;orderId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Reason for cancellation&quot;&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;reason&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// No [Description] — not exposed as a tool&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;GetInternalAuditLogAsync&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it. The source generator produces a fully typed &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; subclass for each described method, wires up the parameter metadata, and generates a registration extension — all at compile time.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-gets-generated&quot;&gt;What Gets Generated&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For &lt;code dir=&quot;auto&quot;&gt;PlaceOrderAsync&lt;/code&gt; above, the generator emits a class like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IOrderServicePlaceOrderAsyncAITool&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;AIFunction&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IOrderService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_service&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AIFunctionMetadata&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_metadata&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AIFunctionMetadata&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;IOrderServicePlaceOrderAsync&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Description &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Places a new order for a customer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Parameters &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AIFunctionParameterMetadata&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;customerId&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Description &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;The customer identifier&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ParameterType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Guid&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IsRequired &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;sku&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Description &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;The product SKU&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ParameterType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IsRequired &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;quantity&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Description &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Number of units to order&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ParameterType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IsRequired &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Guid&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CustomerId&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Sku&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Quantity&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IOrderServicePlaceOrderAsyncAITool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IOrderService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;service&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;_service &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; service;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AIFunctionMetadata&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Metadata&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; _metadata;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;?&gt; &lt;/span&gt;&lt;span&gt;InvokeCoreAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IEnumerable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;KeyValuePair&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;?&gt;&gt;? &lt;/span&gt;&lt;span&gt;arguments&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cancellationToken&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// argument extraction and service call&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; _service.&lt;/span&gt;&lt;span&gt;PlaceOrderAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.CustomerId, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.Sku, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.Quantity);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A second class is generated for &lt;code dir=&quot;auto&quot;&gt;CancelOrderAsync&lt;/code&gt;. The &lt;code dir=&quot;auto&quot;&gt;GetInternalAuditLogAsync&lt;/code&gt; method is skipped because it has no &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;registration&quot;&gt;Registration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;All generated tools are registered with a single call:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddGeneratedAITools&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This registers each tool as &lt;code dir=&quot;auto&quot;&gt;Transient&amp;#x3C;AITool, GeneratedToolClass&gt;&lt;/code&gt;. You can then resolve all tools and pass them to any &lt;code dir=&quot;auto&quot;&gt;IChatClient&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tools&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; serviceProvider.&lt;/span&gt;&lt;span&gt;GetServices&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AITool&lt;/span&gt;&lt;span&gt;&gt;().&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatOptions&lt;/span&gt;&lt;span&gt; { Tools &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tools };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; chatClient.&lt;/span&gt;&lt;span&gt;GetResponseAsync&lt;/span&gt;&lt;span&gt;(messages, options);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;conditional-generation&quot;&gt;Conditional Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The AI tool code is only generated when &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt; is referenced in your project. If you don’t reference it, the &lt;code dir=&quot;auto&quot;&gt;[Tool]&lt;/code&gt; attribute still compiles (it’s just an attribute), but no &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; classes or registration code are emitted. This means existing projects that add the DI package won’t get unexpected dependencies.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;aot-safe-argument-extraction&quot;&gt;AOT-Safe Argument Extraction&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The generated &lt;code dir=&quot;auto&quot;&gt;InvokeCoreAsync&lt;/code&gt; handles the &lt;code dir=&quot;auto&quot;&gt;JsonElement&lt;/code&gt;-vs-already-deserialized argument problem that trips up most hand-written AI tools. For every standard type, the generator emits a direct &lt;code dir=&quot;auto&quot;&gt;JsonElement&lt;/code&gt; accessor:&lt;/p&gt;




























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Extraction&lt;/th&gt;&lt;th&gt;Reflection-free&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;string&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetString()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;int&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;long&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;short&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;byte&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetInt32()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;GetInt64()&lt;/code&gt;, etc.&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bool&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetBoolean()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;double&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;float&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decimal&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetDouble()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;GetSingle()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;GetDecimal()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Guid&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetGuid()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;DateTime&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetDateTime()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;DateTimeOffset&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;GetDateTimeOffset()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;DateOnly&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;TimeOnly&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;TimeSpan&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Parse(GetString())&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Enums&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Enum.Parse&amp;#x3C;T&gt;(GetString())&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Complex types&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;JsonSerializer.Deserialize&amp;#x3C;T&gt;()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Needs &lt;code dir=&quot;auto&quot;&gt;JsonSerializerContext&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If the argument arrives as a &lt;code dir=&quot;auto&quot;&gt;JsonElement&lt;/code&gt; (common when the framework hasn’t pre-deserialized), the correct accessor is used. If it arrives already typed (some frameworks do this), a direct cast is used. Both paths are handled with a single &lt;code dir=&quot;auto&quot;&gt;is JsonElement&lt;/code&gt; check — no try/catch, no &lt;code dir=&quot;auto&quot;&gt;Convert.ChangeType&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;cancellationtoken-handling&quot;&gt;CancellationToken Handling&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If your service method accepts a &lt;code dir=&quot;auto&quot;&gt;CancellationToken&lt;/code&gt;, the generator does the right thing automatically:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Searches products&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;SearchAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Search query&quot;&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cancellationToken&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// not exposed as a tool parameter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;CancellationToken&lt;/code&gt; is excluded from the tool’s parameter metadata and properties. In &lt;code dir=&quot;auto&quot;&gt;InvokeCoreAsync&lt;/code&gt;, it’s passed through from the framework’s cancellation token — not extracted from the argument dictionary.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;methods-without-description-are-skipped&quot;&gt;Methods Without &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; Are Skipped&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Only methods with &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; become tools. This gives you fine-grained control over what’s exposed to the LLM. Internal methods, admin operations, or anything you don’t want an AI agent calling — just don’t add the attribute.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;works-with-your-existing-di-setup&quot;&gt;Works With Your Existing DI Setup&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;[Tool]&lt;/code&gt; attribute goes on interfaces, while &lt;code dir=&quot;auto&quot;&gt;[Singleton]&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;[Scoped]&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;[Transient]&lt;/code&gt; go on implementation classes — same as before. You keep using &lt;code dir=&quot;auto&quot;&gt;AddGeneratedServices()&lt;/code&gt; for your service registrations and add &lt;code dir=&quot;auto&quot;&gt;AddGeneratedAITools()&lt;/code&gt; alongside it:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddGeneratedServices&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddGeneratedAITools&lt;/span&gt;&lt;span&gt;();  &lt;/span&gt;&lt;span&gt;// only if M.E.AI is referenced&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The two generators are independent. AI tool generation doesn’t affect or depend on your service registrations.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Add &lt;code dir=&quot;auto&quot;&gt;[Tool]&lt;/code&gt; to the interface&lt;/li&gt;
&lt;li&gt;Add &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; to the interface and the methods you want exposed&lt;/li&gt;
&lt;li&gt;Add &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; to parameters (optional but recommended — it helps the LLM)&lt;/li&gt;
&lt;li&gt;Reference &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt; in your project&lt;/li&gt;
&lt;li&gt;Call &lt;code dir=&quot;auto&quot;&gt;services.AddGeneratedAITools()&lt;/code&gt; at startup&lt;/li&gt;
&lt;li&gt;Resolve &lt;code dir=&quot;auto&quot;&gt;IEnumerable&amp;#x3C;AITool&gt;&lt;/code&gt; and pass to your chat client&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Check the &lt;a href=&quot;https://www.shinylib.net/di/&quot;&gt;DI documentation&lt;/a&gt; for the full setup guide and the &lt;a href=&quot;https://www.shinylib.net/di/release-notes/&quot;&gt;release notes&lt;/a&gt; for the complete changelog.&lt;/p&gt;</content:encoded><category>Release</category><category>AI</category><category>DI</category></item><item><title>One Contract, Three Transports — Mediator AI Tooling</title><link>https://www.shinylib.net/blog/2026/04/mediator-ai-tools/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/04/mediator-ai-tools/</guid><pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;What if you could write a single C# record and have it automatically become a fully typed AI tool — with zero adapter code? That’s what Shiny Mediator 6.3 delivers.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Building AI tool calling today means writing repetitive adapter code. You define a JSON schema by hand, parse arguments from the LLM response, validate them, call your business logic, and serialize the result back. If you already have a mediator contract for the same operation, you’re duplicating intent across two representations. Multiply that by every tool your agent needs — ten, twenty, fifty tools — and it becomes a real maintenance problem.&lt;/p&gt;
&lt;p&gt;Worse, the schema and the code drift apart. You rename a property in your contract but forget to update the JSON schema. You add a new required parameter but the tool adapter still treats it as optional. The LLM hallucinates a parameter name that &lt;em&gt;used to&lt;/em&gt; exist, and your hand-written parser silently swallows the error. These bugs are subtle, hard to test, and only surface at runtime.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-contract-first-approach&quot;&gt;The Contract-First Approach&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;In Shiny Mediator, a &lt;strong&gt;contract&lt;/strong&gt; is a plain record that describes an operation:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Get the current weather forecast for a given city&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetWeather&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;property&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;The city name to get weather for&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;City&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;property&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Temperature unit: &apos;celsius&apos; or &apos;fahrenheit&apos;&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Unit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;celsius&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;WeatherResult&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WeatherResult&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;City&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Temperature&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Unit&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Condition&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And a &lt;strong&gt;handler&lt;/strong&gt; implements the logic:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MediatorSingleton&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetWeatherHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetWeather&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;WeatherResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;WeatherResult&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;GetWeather&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// your logic here&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s the only code you write. From here, source generators take over.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ai-tool-generation&quot;&gt;AI Tool Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Add a &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; attribute to your contract and set &lt;code dir=&quot;auto&quot;&gt;ShinyMediatorGenerateAITools=true&lt;/code&gt; in your project:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;PropertyGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ShinyMediatorGenerateAITools&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ShinyMediatorGenerateAITools&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;PropertyGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The source generator produces a fully typed &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; subclass compatible with &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// auto-generated&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;internal&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sealed&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetWeatherAIFunction&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;AIFunction&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_mediator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JsonElement&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_jsonSchema&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JsonDocument.&lt;/span&gt;&lt;span&gt;Parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;type&quot;: &quot;object&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;properties&quot;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;city&quot;: { &quot;description&quot;: &quot;The city name to get weather for&quot;, &quot;type&quot;: &quot;string&quot; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;unit&quot;: { &quot;description&quot;: &quot;Temperature unit&quot;, &quot;type&quot;: &quot;string&quot;, &quot;default&quot;: &quot;celsius&quot; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;required&quot;: [&quot;city&quot;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span&gt;).RootElement.&lt;/span&gt;&lt;span&gt;Clone&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;GetWeather&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Get the current weather forecast for a given city&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JsonElement&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JsonSchema&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; _jsonSchema;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;override&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ValueTask&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;?&gt; &lt;/span&gt;&lt;span&gt;InvokeCoreAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;AIFunctionArguments&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;arguments&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cancellationToken&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; JsonSerializer.&lt;/span&gt;&lt;span&gt;SerializeToElement&lt;/span&gt;&lt;span&gt;(arguments);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;contract&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetWeather&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;City&lt;/span&gt;&lt;span&gt;: json.&lt;/span&gt;&lt;span&gt;GetProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;city&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;GetString&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;Unit&lt;/span&gt;&lt;span&gt;: json.&lt;/span&gt;&lt;span&gt;TryGetProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;unit&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; u.ValueKind &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; JsonValueKind.Null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; u.&lt;/span&gt;&lt;span&gt;GetString&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;celsius&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; _mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;WeatherResult&lt;/span&gt;&lt;span&gt;&gt;(contract, cancellationToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; result;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A registration extension is also generated:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AddMediatorRegistry&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AddGeneratedAITools&lt;/span&gt;&lt;span&gt;()   &lt;/span&gt;&lt;span&gt;// registers every [Description] contract as an AITool&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Then pass the tools to any &lt;code dir=&quot;auto&quot;&gt;IChatClient&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tools&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; services.&lt;/span&gt;&lt;span&gt;GetServices&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AITool&lt;/span&gt;&lt;span&gt;&gt;().&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatOptions&lt;/span&gt;&lt;span&gt; { Tools &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tools };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; chatClient.&lt;/span&gt;&lt;span&gt;GetResponseAsync&lt;/span&gt;&lt;span&gt;(history, options);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;middleware-runs-on-ai-tool-calls-too&quot;&gt;Middleware Runs on AI Tool Calls Too&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Because the generated AI tools dispatch through the mediator pipeline, every middleware you’ve already configured applies to AI tool calls automatically. Logging, validation, authorization, exception handling, caching — all of it fires without any extra wiring.&lt;/p&gt;
&lt;p&gt;This is a significant advantage over hand-rolled &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; implementations. When you write a tool adapter manually, it typically calls your service layer directly, bypassing cross-cutting concerns. With the mediator approach, an AI tool call follows the same pipeline as a UI-triggered action or an API call. Your audit log captures it. Your validation middleware rejects bad input before the handler runs. Your error handling middleware catches exceptions and returns structured errors the LLM can interpret.&lt;/p&gt;
&lt;p&gt;You can even write middleware that targets AI calls specifically — for example, injecting a &lt;code dir=&quot;auto&quot;&gt;MediatorContext&lt;/code&gt; value that tells the handler the call originated from an LLM, so you can apply tighter authorization or rate limiting for AI-initiated operations.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;scaling-to-many-tools&quot;&gt;Scaling to Many Tools&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The real power shows when your agent needs many tools. Instead of maintaining dozens of &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; subclasses with hand-written schemas, you just add &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; to your existing contracts. Every contract with a description attribute becomes a tool at the next build.&lt;/p&gt;
&lt;p&gt;Adding a new tool to your agent is the same workflow as adding any new mediator operation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define the contract record with &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Implement the handler&lt;/li&gt;
&lt;li&gt;Done — the tool is registered automatically&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No schema files to maintain. No adapter classes to write. No registration code to update. The source generator handles the JSON schema, argument parsing, DI wiring, and &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; implementation.&lt;/p&gt;
&lt;p&gt;This also means removing a tool is just deleting the &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; attribute (or the contract itself). There are no orphaned adapters or stale schema definitions to clean up.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;beyond-ai-the-same-contract-powers-http-too&quot;&gt;Beyond AI: The Same Contract Powers HTTP Too&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The same contract-first approach extends beyond AI tooling. Shiny Mediator also generates HTTP clients and ASP.NET endpoints from your contracts — meaning a single record and handler can serve as an AI tool, a typed HTTP client, and a REST endpoint simultaneously. The transports are generated; you write the logic once.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Traditional tool-calling setups require you to maintain parallel definitions:&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;Without Mediator&lt;/th&gt;&lt;th&gt;With Mediator&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Business logic&lt;/td&gt;&lt;td&gt;Handler class&lt;/td&gt;&lt;td&gt;Handler class&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AI tool schema&lt;/td&gt;&lt;td&gt;Manual JSON schema&lt;/td&gt;&lt;td&gt;Generated from contract&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AI tool adapter&lt;/td&gt;&lt;td&gt;Manual AIFunction subclass&lt;/td&gt;&lt;td&gt;Generated&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Argument parsing&lt;/td&gt;&lt;td&gt;Manual deserialization&lt;/td&gt;&lt;td&gt;Generated&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DI registration&lt;/td&gt;&lt;td&gt;Manual for each tool&lt;/td&gt;&lt;td&gt;Generated&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Middleware/validation&lt;/td&gt;&lt;td&gt;Manual per tool&lt;/td&gt;&lt;td&gt;Automatic via pipeline&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;With the contract-first approach, adding a new capability to your application — whether it’s exposed as an AI tool, an HTTP endpoint, or both — is one record and one handler.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;full-aot-compliance&quot;&gt;Full AOT Compliance&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The generated &lt;code dir=&quot;auto&quot;&gt;AIFunction&lt;/code&gt; classes are fully Native AOT compatible. Here’s what makes that possible:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No reflection.&lt;/strong&gt; The generator reads &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; attributes, property types, nullability, and default values at compile time. It emits direct property access code — &lt;code dir=&quot;auto&quot;&gt;json.GetProperty(&quot;city&quot;).GetString()!&lt;/code&gt; — instead of relying on &lt;code dir=&quot;auto&quot;&gt;JsonSerializer.Deserialize&amp;#x3C;T&gt;()&lt;/code&gt; or reflection-based binding.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static JSON schema.&lt;/strong&gt; The schema is a compile-time constant string parsed once into a &lt;code dir=&quot;auto&quot;&gt;JsonElement&lt;/code&gt; on first use. There’s no runtime schema construction, no &lt;code dir=&quot;auto&quot;&gt;JsonSerializerOptions&lt;/code&gt; configuration, and no dynamic type inspection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constructor-based hydration.&lt;/strong&gt; The generated code constructs the contract using its primary constructor with named arguments. No &lt;code dir=&quot;auto&quot;&gt;Activator.CreateInstance&lt;/code&gt;, no &lt;code dir=&quot;auto&quot;&gt;FormatterServices&lt;/code&gt;, no property setters via reflection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete types throughout.&lt;/strong&gt; Each generated class is a sealed, non-generic concrete type. The DI registrations are explicit &lt;code dir=&quot;auto&quot;&gt;AddSingleton&amp;#x3C;AITool&gt;(sp =&gt; new GetWeatherAIFunction(...))&lt;/code&gt; calls — no open generics or service descriptor scanning at runtime.&lt;/p&gt;
&lt;p&gt;This means your AI tools work in trimmed, ahead-of-time compiled applications — including .NET MAUI apps targeting iOS and Android — without linker warnings or runtime failures. The same tools that power your cloud API also run on-device in a fully native binary.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;supported-type-mappings&quot;&gt;Supported Type Mappings&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The generator handles the full range of C# types in your contracts:&lt;/p&gt;


















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;C# Type&lt;/th&gt;&lt;th&gt;JSON Schema&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;string&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Guid&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Uri&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DateTime&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;string&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bool&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;boolean&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;int&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;long&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;short&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;byte&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;integer&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;float&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;double&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;decimal&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;number&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;enum&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;string&quot;&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;&quot;enum&quot;&lt;/code&gt; array&lt;/td&gt;&lt;td&gt;All values listed for the LLM&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;T[]&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;IEnumerable&amp;#x3C;T&gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;&quot;array&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Nullable types (&lt;code dir=&quot;auto&quot;&gt;T?&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Omitted from &lt;code dir=&quot;auto&quot;&gt;&quot;required&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Default values&lt;/td&gt;&lt;td&gt;Included as &lt;code dir=&quot;auto&quot;&gt;&quot;default&quot;&lt;/code&gt; in schema&lt;/td&gt;&lt;td&gt;Fallback used when LLM omits the parameter&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ICommand&lt;/code&gt; contracts are also supported — the generated tool returns a success message string instead of a typed result.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Add the &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; attribute to your contracts and their properties&lt;/li&gt;
&lt;li&gt;Set &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;ShinyMediatorGenerateAITools&gt;true&amp;#x3C;/ShinyMediatorGenerateAITools&gt;&lt;/code&gt; in your project file&lt;/li&gt;
&lt;li&gt;Reference &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Call &lt;code dir=&quot;auto&quot;&gt;.AddGeneratedAITools()&lt;/code&gt; during mediator setup&lt;/li&gt;
&lt;li&gt;Resolve &lt;code dir=&quot;auto&quot;&gt;IEnumerable&amp;#x3C;AITool&gt;&lt;/code&gt; from DI and pass to your chat client&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every contract with a &lt;code dir=&quot;auto&quot;&gt;[Description]&lt;/code&gt; attribute automatically becomes a tool. Add a new contract, and the next build picks it up — no registration changes, no schema files, no adapter classes.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href=&quot;https://github.com/shinyorg/mediator/tree/main/samples/Sample.CopilotConsole&quot;&gt;Sample.CopilotConsole&lt;/a&gt; for a working example that wires up AI tools with a chat loop, or browse the &lt;a href=&quot;https://www.shinylib.net/mediator/&quot;&gt;Mediator documentation&lt;/a&gt; for the full setup guide.&lt;/p&gt;</content:encoded><category>Release</category><category>AI</category><category>Mediator</category></item><item><title>AI-Powered Navigation in Shiny MAUI Shell</title><link>https://www.shinylib.net/blog/2026/04/shiny-shell-ai/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/04/shiny-shell-ai/</guid><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;What if your app could understand “My furnace is broken — it’s urgent!” and automatically open the right form with the description filled in and the priority set to Urgent? That’s exactly what the new AI integration in Shiny MAUI Shell does.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-idea&quot;&gt;The Idea&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Mobile apps have dozens of pages. Users have to know where things are, tap through menus, and manually fill in fields. But with AI chat becoming the norm, we asked: &lt;em&gt;what if the AI could navigate your app for you?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Shiny Shell’s source generator already knows every route in your app and every parameter each page accepts. We just needed to make that metadata available to an AI model — and give it a way to act on what it discovers.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;two-tools-any-number-of-pages&quot;&gt;Two Tools, Any Number of Pages&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Instead of registering a separate AI tool for every page (which doesn’t scale), we generate just two:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;GetAiToolApplicableGeneratedRoutes()&lt;/code&gt;&lt;/strong&gt; — returns all routes that have intent descriptions and parameters. The AI calls this to discover what pages exist and what they do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;NavigateToRoute()&lt;/code&gt;&lt;/strong&gt; — accepts a route name and a &lt;code dir=&quot;auto&quot;&gt;Dictionary&amp;#x3C;string, string&gt;&lt;/code&gt; of parameters. The AI calls this to navigate and pre-fill the form.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s it. Add a new page with &lt;code dir=&quot;auto&quot;&gt;[ShellMap]&lt;/code&gt; descriptions and &lt;code dir=&quot;auto&quot;&gt;[ShellProperty]&lt;/code&gt; inference hints, and the AI automatically discovers it. No tool registration changes needed.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;describe-intent-not-pages&quot;&gt;Describe Intent, Not Pages&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The key insight is that descriptions should express &lt;strong&gt;user intent&lt;/strong&gt;, not page names:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Good — the AI matches &quot;my pipe burst&quot; to this route&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ShellMap&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;WorkOrderPage&lt;/span&gt;&lt;span&gt;&gt;(description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Use when the user reports something broken&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;malfunctioning, needing repair, maintenance, or service&lt;/span&gt;&lt;span&gt;&quot;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Bad — the AI has to guess what &quot;Work order page&quot; means&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ShellMap&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;WorkOrderPage&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Work order page&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Similarly, property descriptions tell the AI how to &lt;strong&gt;infer&lt;/strong&gt; values from natural language. Properties can use real types — enums, ints, bools — and the generator handles conversion automatically:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enum&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WorkOrderPriority&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;Low&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Medium&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;High&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Urgent&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ShellProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Summarize what is broken based on what the user said&quot;&lt;/span&gt;&lt;span&gt;, required&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; Description { get; set; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;.Empty;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ShellProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Infer urgency from the user&apos;s tone. Must be: Low, Medium, High, or Urgent&quot;&lt;/span&gt;&lt;span&gt;, required&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; WorkOrderPriority Priority { get; set; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; WorkOrderPriority.Medium;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The AI sends &lt;code dir=&quot;auto&quot;&gt;&quot;Urgent&quot;&lt;/code&gt; as a string, and the generated &lt;code dir=&quot;auto&quot;&gt;NavigateToRoute&lt;/code&gt; converts it to &lt;code dir=&quot;auto&quot;&gt;WorkOrderPriority.Urgent&lt;/code&gt; via case-insensitive &lt;code dir=&quot;auto&quot;&gt;Enum.Parse&lt;/code&gt;. The same works for &lt;code dir=&quot;auto&quot;&gt;int&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;bool&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;double&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DateTime&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Guid&lt;/code&gt;, and other common types.&lt;/p&gt;
&lt;p&gt;Note that AI-compatible ViewModels do &lt;strong&gt;not&lt;/strong&gt; need to implement &lt;code dir=&quot;auto&quot;&gt;IQueryAttributable&lt;/code&gt;. The generated &lt;code dir=&quot;auto&quot;&gt;NavigateToRoute&lt;/code&gt; sets &lt;code dir=&quot;auto&quot;&gt;[ShellProperty]&lt;/code&gt; properties directly on the ViewModel instance — no query attribute plumbing required.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-gets-generated&quot;&gt;What Gets Generated&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The source generator produces &lt;code dir=&quot;auto&quot;&gt;GeneratedRouteInfo&lt;/code&gt; metadata with full parameter schemas:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GeneratedRouteInfo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Route&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;GeneratedRouteParameter&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;Parameters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GeneratedRouteParameter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParameterName&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TypeName&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IsRequired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The AI model sees the route descriptions, parameter names, types, requirements, and inference hints — everything it needs to match intent and extract values.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;wiring-it-up&quot;&gt;Wiring It Up&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AI extensions are now &lt;strong&gt;enabled by default&lt;/strong&gt; — just install &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.AI&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Microsoft.Extensions.AI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Register the generated &lt;code dir=&quot;auto&quot;&gt;AiMauiShellTools&lt;/code&gt; class via the &lt;code dir=&quot;auto&quot;&gt;AddAiTools()&lt;/code&gt; extension:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.&lt;/span&gt;&lt;span&gt;UseShinyShell&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AddGeneratedMaps&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AddAiTools&lt;/span&gt;&lt;span&gt;()          &lt;/span&gt;&lt;span&gt;// registers AiMauiShellTools as singleton&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Then inject &lt;code dir=&quot;auto&quot;&gt;AiMauiShellTools&lt;/code&gt; wherever you need AI-powered navigation. It provides a &lt;code dir=&quot;auto&quot;&gt;Prompt&lt;/code&gt; property (pre-formatted route descriptions for seeding system messages) and a &lt;code dir=&quot;auto&quot;&gt;Tools&lt;/code&gt; property (ready-to-use &lt;code dir=&quot;auto&quot;&gt;AITool[]&lt;/code&gt;):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AiMauiShellTools&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;aiTools&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// Seed the system prompt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;history.&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatMessage&lt;/span&gt;&lt;span&gt;(ChatRole.System, aiTools.Prompt));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// Use the tools&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ChatOptions&lt;/span&gt;&lt;span&gt; { Tools &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt; aiTools.Tools] };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The class name is customizable via the &lt;code dir=&quot;auto&quot;&gt;ShinyMauiShell_AiToolsClassName&lt;/code&gt; MSBuild property if &lt;code dir=&quot;auto&quot;&gt;AiMauiShellTools&lt;/code&gt; doesn’t fit your naming conventions.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it-in-the-sample&quot;&gt;Try It in the Sample&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The sample app includes a full working demo with GitHub Copilot authentication. Users authenticate with their own GitHub account through the OAuth device flow, and the app uses the Copilot API as the chat backend. Try saying things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“My furnace is not working! URGENT”&lt;/em&gt; — opens the work order form with description and priority filled in&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“I’d like to discuss a partnership. My name is Allan, email &lt;a href=&quot;mailto:allan@test.com&quot;&gt;allan@test.com&lt;/a&gt;”&lt;/em&gt; — opens the contact form with fields populated&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Check out the &lt;a href=&quot;https://www.shinylib.net/mauishell/ai/&quot;&gt;AI Integration documentation&lt;/a&gt; for the full setup guide, or browse the &lt;a href=&quot;https://github.com/shinyorg/mauishell&quot;&gt;sample code on GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Release</category><category>AI</category><category>MAUI Shell</category></item><item><title>Shiny Client v4 - Windows Support, .NET 10, and a Ton of Improvements</title><link>https://www.shinylib.net/blog/2026/03/client-v4/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/03/client-v4/</guid><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It’s been a long road, but Shiny Client v4 is here. This is a major release that brings Windows support, moves to .NET 10, and packs in a significant number of fixes and enhancements
across almost every module. Let’s dig in.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;windows-support&quot;&gt;Windows Support&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The headline feature of v4 — &lt;strong&gt;BluetoothLE, BLE Hosting, HTTP Transfers, and Locations&lt;/strong&gt; all now work on Windows. Background support isn’t available yet on the Windows platform, but
foreground scenarios are fully supported. This opens up a whole new set of use cases for desktop and kiosk applications built with .NET MAUI.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;net-10&quot;&gt;.NET 10&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;v4 enforces &lt;code dir=&quot;auto&quot;&gt;net10.0&lt;/code&gt; target frameworks across the board. This is a breaking change, so make sure your projects are targeting .NET 10 before upgrading.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;configuration-extensions&quot;&gt;Configuration Extensions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shinyorg/remoteconfig&quot;&gt;HTTP Remote Configuration&lt;/a&gt; has been moved directly into &lt;code dir=&quot;auto&quot;&gt;Shiny.Extensions.Configuration&lt;/code&gt;. No more separate package — remote config is now a first-class citizen in the configuration stack.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;locations&quot;&gt;Locations&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Locations got a lot of love in this release, particularly on iOS where we’ve adopted the newer Apple APIs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iOS 18+&lt;/strong&gt; now uses &lt;code dir=&quot;auto&quot;&gt;CLMonitor&lt;/code&gt; for GPS — the modern replacement for the legacy location APIs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New geofence registration mechanics&lt;/strong&gt; for iOS 17+ using the new &lt;code dir=&quot;auto&quot;&gt;CLMonitor&lt;/code&gt; API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geofence Manager RequestState&lt;/strong&gt; now works correctly on the new CLMonitor API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPS background permission&lt;/strong&gt; on iOS is now requested immediately instead of waiting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GpsDelegate&lt;/strong&gt; now has a boolean to detect if the device is stationary — useful for battery optimization and movement detection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GpsDelegate batch fix&lt;/strong&gt; — the base calculations could receive a batch and trigger multiple calculations. This is now a synchronized operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android background location permission&lt;/strong&gt; — no longer requests &lt;code dir=&quot;auto&quot;&gt;ACCESS_BACKGROUND_LOCATION&lt;/code&gt; unless realtime GPS with less than API 31 or standard background tracking is being used&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;http-transfers&quot;&gt;HTTP Transfers&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;HTTP Transfers received some of the most impactful enhancements in v4:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New transfer types&lt;/strong&gt; — Transfers can now be &lt;code dir=&quot;auto&quot;&gt;UploadMultipart&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;UploadRaw&lt;/code&gt; (body is raw bytes), or &lt;code dir=&quot;auto&quot;&gt;Download&lt;/code&gt;. The &lt;code dir=&quot;auto&quot;&gt;UploadRaw&lt;/code&gt; type is critical for sending directly to services like Azure Blob Storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure Blob Storage helper&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;AzureBlobStorageRequest.CreateForAzureBlobStorage&lt;/code&gt; static helper method makes it dead simple to queue uploads to Azure Blob Storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New HttpTransferDelegate&lt;/strong&gt; — allows you to set retries and detect denied authorization, enabling you to refresh your token and issue a new request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread-safe HttpTransferMonitor&lt;/strong&gt; — now uses a thread-safe &lt;code dir=&quot;auto&quot;&gt;BindingList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android file validation&lt;/strong&gt; — uploads now verify the file exists before queuing, and download directories are checked before queuing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iOS special character fix&lt;/strong&gt; — filenames with special characters are now sent properly, along with improved form data upload&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;push-notifications&quot;&gt;Push Notifications&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iOS raw notification data&lt;/strong&gt; — you can now &lt;code dir=&quot;auto&quot;&gt;#if IOS&lt;/code&gt; to get an &lt;code dir=&quot;auto&quot;&gt;AppleNotification&lt;/code&gt; that contains the raw &lt;code dir=&quot;auto&quot;&gt;NSDictionary&lt;/code&gt; for full access to the push payload&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure Notification Hubs&lt;/strong&gt; now supports template registrations for more flexible push scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;bluetoothle&quot;&gt;BluetoothLE&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ManagedScanResult&lt;/strong&gt; now includes the full advertisement data, giving you access to native internals when you need them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improved manufacturer data parsing&lt;/strong&gt; on Android&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLE Delegate&lt;/strong&gt; now reports proper status changes for adapter enabled state on Android&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legacy scanning disabled&lt;/strong&gt; on newer Android versions for better scan performance&lt;/li&gt;
&lt;li&gt;Multiple &lt;strong&gt;thread safety improvements&lt;/strong&gt; for ManagedScan- &lt;strong&gt;Peripheral cleanup&lt;/strong&gt; on Android now matches iOS behavior for consistency&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;jobs&quot;&gt;Jobs&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Android fix&lt;/strong&gt; — successful jobs that run too long often had the Android completion handler already disposed. This has been resolved.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The main thing to watch for:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update your target frameworks to &lt;code dir=&quot;auto&quot;&gt;net10.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If you were using the separate remote config package, switch to &lt;code dir=&quot;auto&quot;&gt;Shiny.Extensions.Configuration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;HTTP Transfer &lt;code dir=&quot;auto&quot;&gt;Request&lt;/code&gt; types have changed — review the new &lt;code dir=&quot;auto&quot;&gt;UploadMultipart&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;UploadRaw&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;Download&lt;/code&gt; options&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Check out the full &lt;a href=&quot;https://www.shinylib.net/client/release-notes&quot;&gt;release notes&lt;/a&gt; for every detail, and head over to our &lt;a href=&quot;https://www.shinylib.net/&quot;&gt;documentation&lt;/a&gt; to get started.&lt;/p&gt;
&lt;p&gt;As always, feedback and contributions are welcome on &lt;a href=&quot;https://github.com/shinyorg/shiny&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Client</category><category>Release</category></item><item><title>Shiny DocumentDb v3: One API, Four Databases</title><link>https://www.shinylib.net/blog/2026/03/documentdb-v3/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/03/documentdb-v3/</guid><description>Shiny.SqliteDocumentDb is now Shiny.DocumentDb — the same zero-schema document store API, now supporting SQLite, SQL Server, MySQL, and PostgreSQL.

</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Shiny.SqliteDocumentDb&lt;/code&gt; has been rewritten as &lt;strong&gt;Shiny.DocumentDb&lt;/strong&gt; — a multi-provider document store that keeps the same developer-friendly API while supporting four database backends.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-new-in-v3&quot;&gt;What’s new in v3&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQL Server&lt;/strong&gt; support via &lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.SqlServer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL&lt;/strong&gt; support via &lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.MySql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; support via &lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.PostgreSql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLite&lt;/strong&gt; continues to work via &lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.Sqlite&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Same API&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;IDocumentStore&lt;/code&gt;, fluent queries, projections, aggregates, transactions, AOT support — all provider-agnostic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DI extensions&lt;/strong&gt; bundled into each provider package — no separate DI package needed&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;provider-setup&quot;&gt;Provider setup&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Each provider follows the same pattern — install the provider package and register with DI:&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;sqlite&quot;&gt;SQLite&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet add package Shiny.DocumentDb.Sqlite&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DocumentDb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sqlite&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;sql-server&quot;&gt;SQL Server&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet add package Shiny.DocumentDb.SqlServer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DocumentDb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SqlServer&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqlServerDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Server=localhost;Database=mydb;Trusted_Connection=true;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;mysql&quot;&gt;MySQL&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet add package Shiny.DocumentDb.MySql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DocumentDb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MySql&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddMySqlDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Server=localhost;Database=mydb;User=root;Password=pass;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;postgresql&quot;&gt;PostgreSQL&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet add package Shiny.DocumentDb.PostgreSql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DocumentDb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PostgreSql&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddPostgreSqlDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Host=localhost;Database=mydb;Username=postgres;Password=pass;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-from-v2&quot;&gt;Migration from v2&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;package-changes&quot;&gt;Package changes&lt;/h3&gt;&lt;/div&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;v2 Package&lt;/th&gt;&lt;th&gt;v3 Package&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Shiny.SqliteDocumentDb&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Shiny.DocumentDb.Sqlite&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Shiny.SqliteDocumentDb.Extensions.DependencyInjection&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;(included in provider package)&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;code-changes&quot;&gt;Code changes&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. Update using statements:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SqliteDocumentDb&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SqliteDocumentDb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Extensions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DependencyInjection&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DocumentDb&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DocumentDb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sqlite&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Update DI registration:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; opts.ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// simple overload unchanged&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.DatabaseProvider &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDatabaseProvider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Update direct instantiation:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;DatabaseProvider &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDatabaseProvider&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Or use the convenience constructor (unchanged)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;4. &lt;code dir=&quot;auto&quot;&gt;IDocumentStore&lt;/code&gt; usage is unchanged&lt;/strong&gt; — all query, CRUD, projection, aggregate, and transaction code works exactly the same.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;documentation&quot;&gt;Documentation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Full documentation is available at &lt;a href=&quot;https://www.shinylib.net/data/documentdb/&quot;&gt;/data/documentdb/&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>documentdb</category><category>release</category></item><item><title>Shiny.SqliteDocumentDb v2.0.0</title><link>https://www.shinylib.net/blog/2026/03/sqlitedocumentdb-v2/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/03/sqlitedocumentdb-v2/</guid><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;a href=&quot;https://www.nuget.org/packages/Shiny.SqliteDocumentDb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://img.shields.io/nuget/v/Shiny.SqliteDocumentDb?style=for-the-badge&amp;#x26;logo=nuget&quot; alt=&quot;NuGet package Shiny.SqliteDocumentDb&quot;&gt;&lt;/a&gt;
&lt;a href=&quot;https://www.nuget.org/packages/Shiny.SqliteDocumentDb.Extensions.DependencyInjection&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://img.shields.io/nuget/v/Shiny.SqliteDocumentDb.Extensions.DependencyInjection?style=for-the-badge&amp;#x26;logo=nuget&quot; alt=&quot;NuGet package Shiny.SqliteDocumentDb.Extensions.DependencyInjection&quot;&gt;&lt;/a&gt;
&lt;p&gt;v2.0.0 is now available. This release focuses on flexibility — you can now map document types to dedicated SQLite tables, use custom Id properties, diff objects against stored documents, batch insert collections efficiently, customize the default table name, and use the core library without any dependency injection framework.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;breaking-changes&quot;&gt;Breaking Changes&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;di-extensions-moved-to-a-separate-package&quot;&gt;DI Extensions Moved to a Separate Package&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.DependencyInjection&lt;/code&gt; support has been extracted into its own package:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.SqliteDocumentDb.Extensions.DependencyInjection&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The core &lt;code dir=&quot;auto&quot;&gt;Shiny.SqliteDocumentDb&lt;/code&gt; package no longer depends on &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.DependencyInjection.Abstractions&lt;/code&gt;. If you use &lt;code dir=&quot;auto&quot;&gt;AddSqliteDocumentStore()&lt;/code&gt;, add the new package. If you instantiate &lt;code dir=&quot;auto&quot;&gt;SqliteDocumentStore&lt;/code&gt; directly, no changes are needed.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;new-features&quot;&gt;New Features&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;convenience-constructor&quot;&gt;Convenience Constructor&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;You can now create a store with just a connection string:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;custom-default-table-name&quot;&gt;Custom Default Table Name&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The shared document table is no longer hardcoded to &lt;code dir=&quot;auto&quot;&gt;&quot;documents&quot;&lt;/code&gt;. Set &lt;code dir=&quot;auto&quot;&gt;TableName&lt;/code&gt; on options to use any name:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TableName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;my_documents&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;table-per-type-mapping&quot;&gt;Table-Per-Type Mapping&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;You can now map specific document types to their own dedicated SQLite tables. Unmapped types continue to share the default table.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TableName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;documents&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()           &lt;/span&gt;&lt;span&gt;// auto-derives table name → &quot;User&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;orders&quot;&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;// explicit table name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&amp;#x3C;T&gt;()&lt;/code&gt;&lt;/strong&gt; — auto-derives the table name from the type using the configured &lt;code dir=&quot;auto&quot;&gt;TypeNameResolution&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&amp;#x3C;T&gt;(string tableName)&lt;/code&gt;&lt;/strong&gt; — maps to an explicit table name&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fluent API&lt;/strong&gt; — calls chain for concise configuration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duplicate protection&lt;/strong&gt; — mapping two types to the same table throws &lt;code dir=&quot;auto&quot;&gt;ArgumentException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AOT-safe&lt;/strong&gt; — type names are resolved at registration time, not at runtime&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tables are lazily created on first use with the same schema (&lt;code dir=&quot;auto&quot;&gt;Id&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;TypeName&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Data&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;CreatedAt&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;UpdatedAt&lt;/code&gt;) and composite primary key. This works seamlessly with all store operations including transactions, the fluent query builder, projections, indexes, and streaming.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;custom-id-properties&quot;&gt;Custom Id Properties&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Types mapped to a dedicated table can use an alternate property as the document Id instead of the default &lt;code dir=&quot;auto&quot;&gt;Id&lt;/code&gt;. The property must be &lt;code dir=&quot;auto&quot;&gt;Guid&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;int&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;long&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;string&lt;/code&gt; — the same types supported for the standard &lt;code dir=&quot;auto&quot;&gt;Id&lt;/code&gt; property.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Customer&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;customers&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; c.CustomerId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Sensor&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;sensors&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; s.DeviceKey)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;All four &lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&lt;/code&gt; overloads support this:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Overload&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&amp;#x3C;T&gt;()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Auto-derive table name, default &lt;code dir=&quot;auto&quot;&gt;Id&lt;/code&gt; property&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&amp;#x3C;T&gt;(tableName)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Explicit table name, default &lt;code dir=&quot;auto&quot;&gt;Id&lt;/code&gt; property&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&amp;#x3C;T&gt;(idProperty)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Auto-derive table name, custom Id property&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&amp;#x3C;T&gt;(tableName, idProperty)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Explicit table name, custom Id property&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Auto-generation rules still apply — &lt;code dir=&quot;auto&quot;&gt;Guid&lt;/code&gt; and numeric Ids are auto-generated when default, and the value is written back to the mapped property after insert. Custom Id remapping is only available through &lt;code dir=&quot;auto&quot;&gt;MapTypeToTable&lt;/code&gt;, keeping the shared table convention simple.&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;example-mixed-mapped-and-unmapped-types&quot;&gt;Example: Mixed Mapped and Unmapped Types&lt;/h4&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;orders&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Device&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;devices&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; d.SerialNumber);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(options);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Users are stored in the &quot;User&quot; table (Id property)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; { Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;u1&quot;&lt;/span&gt;&lt;span&gt;, Name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;, Age &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;25&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Orders are stored in the &quot;orders&quot; table (Id property)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt; { Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;o1&quot;&lt;/span&gt;&lt;span&gt;, CustomerName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;, Status &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Pending&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Devices are stored in the &quot;devices&quot; table (SerialNumber property as Id)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Device&lt;/span&gt;&lt;span&gt; { SerialNumber &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;SN-001&quot;&lt;/span&gt;&lt;span&gt;, Model &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Sensor-X&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Settings go to the default &quot;documents&quot; table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AppSettings&lt;/span&gt;&lt;span&gt; { Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;global&quot;&lt;/span&gt;&lt;span&gt;, Theme &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Dark&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Queries, transactions, indexes — everything works per-table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;().&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h4 id=&quot;di-registration-with-table-mapping&quot;&gt;DI Registration with Table Mapping&lt;/h4&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;orders&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.&lt;/span&gt;&lt;span&gt;MapTypeToTable&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Device&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;devices&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; d.SerialNumber);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;document-diffing-with-getdiff&quot;&gt;Document Diffing with GetDiff&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Compare a modified object against the stored document and get an RFC 6902 &lt;code dir=&quot;auto&quot;&gt;JsonPatchDocument&amp;#x3C;T&gt;&lt;/code&gt; describing the differences. Returns &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; if the document doesn’t exist. Powered by &lt;a href=&quot;https://www.nuget.org/packages/SystemTextJsonPatch&quot;&gt;SystemTextJsonPatch&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;proposed&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;ord-1&quot;&lt;/span&gt;&lt;span&gt;, CustomerName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;, Status &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Delivered&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ShippingAddress &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;() { City &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Seattle&quot;&lt;/span&gt;&lt;span&gt;, State &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;WA&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Lines &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;() { ProductName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Widget&quot;&lt;/span&gt;&lt;span&gt;, Quantity &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;, UnitPrice &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;8.99m&lt;/span&gt;&lt;span&gt; }],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Tags &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;priority&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;expedited&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;patch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;GetDiff&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;ord-1&quot;&lt;/span&gt;&lt;span&gt;, proposed);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// patch.Operations:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;//   Replace /status → Delivered&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;//   Replace /shippingAddress/city → Seattle&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;//   Replace /shippingAddress/state → WA&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;//   Replace /lines → [...]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;//   Replace /tags → [...]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Apply the patch to any instance of the same type&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Get&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;ord-1&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;patch&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ApplyTo&lt;/span&gt;&lt;span&gt;(current&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The diff is deep — nested objects produce individual property-level operations (e.g. &lt;code dir=&quot;auto&quot;&gt;/shippingAddress/city&lt;/code&gt;), while arrays and collections are replaced as a whole. Works with table-per-type, custom Id, and inside transactions.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;batch-insert&quot;&gt;Batch Insert&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;BatchInsert&lt;/code&gt; inserts multiple documents in a single transaction with prepared command reuse for optimal performance. Returns the count inserted. If any document fails (e.g. duplicate Id), the entire batch is rolled back. Auto-generates IDs for Guid, int, and long Id types.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Enumerable.&lt;/span&gt;&lt;span&gt;Range&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;Select&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$&quot;user-&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, Name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$&quot;User &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, Age &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;BatchInsert&lt;/span&gt;&lt;span&gt;(users); &lt;/span&gt;&lt;span&gt;// single transaction, prepared command reused&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Inside a transaction — uses the existing transaction&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;RunInTransaction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; tx.&lt;/span&gt;&lt;span&gt;BatchInsert&lt;/span&gt;&lt;span&gt;(moreUsers);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; tx.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(singleUser);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// All committed or rolled back together&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-guide&quot;&gt;Migration Guide&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;If you use DI&lt;/strong&gt;, add the new package:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.SqliteDocumentDb.Extensions.DependencyInjection&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;AddSqliteDocumentStore()&lt;/code&gt; API is unchanged — just a different package.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;If you instantiate directly&lt;/strong&gt;, no changes required. The default behavior is identical to v1 — all documents go to a table called &lt;code dir=&quot;auto&quot;&gt;&quot;documents&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Table-per-type and custom Id are opt-in.&lt;/strong&gt; Existing databases continue to work without any changes. You can incrementally adopt table mapping for specific types while keeping everything else in the shared table.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><category>Release</category></item><item><title>Introducing Shiny.Music — Cross-Platform Music Library Access for .NET MAUI</title><link>https://www.shinylib.net/blog/2026/03/shiny-music/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/03/shiny-music/</guid><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here’s something that shouldn’t be hard but is: accessing the music library on a user’s device from .NET MAUI.&lt;/p&gt;
&lt;p&gt;On Android, you need &lt;code dir=&quot;auto&quot;&gt;MediaStore.Audio.Media&lt;/code&gt;, a &lt;code dir=&quot;auto&quot;&gt;ContentResolver&lt;/code&gt;, cursor iteration, and different permission models depending on whether you’re targeting API 33+ or older.  On iOS, you need &lt;code dir=&quot;auto&quot;&gt;MPMediaQuery&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;MPMediaItem&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;AVAudioPlayer&lt;/code&gt;, and an &lt;code dir=&quot;auto&quot;&gt;NSAppleMusicUsageDescription&lt;/code&gt; entry or the app crashes on launch.  There’s no MAUI abstraction.  No popular NuGet package.  You write platform-specific code from scratch every time.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href=&quot;https://github.com/shinyorg/music&quot;&gt;Shiny.Music&lt;/a&gt; — a clean, DI-first API that gives you permission management, metadata queries, playback controls, and file export across both platforms.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Two interfaces, one registration call:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;IMediaLibrary&lt;/code&gt;&lt;/strong&gt; — request permissions, query all tracks, search by title/artist/album, copy track files to app storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;IMusicPlayer&lt;/code&gt;&lt;/strong&gt; — play, pause, resume, stop, seek, with state tracking and completion events&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;MauiProgram.cs&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddShinyMusic&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That registers both interfaces as singletons.  Inject them anywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;quick-start&quot;&gt;Quick Start&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MusicPage&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMediaLibrary&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;library&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMusicPlayer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;player&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MusicPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediaLibrary&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;library&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;IMusicPlayer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;player&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.library &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; library;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.player &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; player;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoadAndPlay&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 1. Request permission&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; library.&lt;/span&gt;&lt;span&gt;RequestPermissionAsync&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (status &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; PermissionStatus.Granted)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 2. Browse the library&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allTracks&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; library.&lt;/span&gt;&lt;span&gt;GetAllTracksAsync&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 3. Or search&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;results&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; library.&lt;/span&gt;&lt;span&gt;SearchTracksAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Bohemian&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 4. Play a track&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; player.&lt;/span&gt;&lt;span&gt;PlayAsync&lt;/span&gt;&lt;span&gt;(results[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 5. Control playback&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Pause&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Resume&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Seek&lt;/span&gt;&lt;span&gt;(TimeSpan.&lt;/span&gt;&lt;span&gt;FromSeconds&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Stop&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-media-library&quot;&gt;The Media Library&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;permissions&quot;&gt;Permissions&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Permissions differ between platforms:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;Permission&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Android 13+&lt;/strong&gt; (API 33)&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;READ_MEDIA_AUDIO&lt;/code&gt;&lt;/td&gt;&lt;td&gt;New granular media permission&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Android 12 and below&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;READ_EXTERNAL_STORAGE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Legacy broad storage permission&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Apple Music usage description&lt;/td&gt;&lt;td&gt;Must be in &lt;code dir=&quot;auto&quot;&gt;Info.plist&lt;/code&gt; or app crashes&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The library handles this automatically.  &lt;code dir=&quot;auto&quot;&gt;RequestPermissionAsync()&lt;/code&gt; prompts the user with the correct platform permission.  &lt;code dir=&quot;auto&quot;&gt;CheckPermissionAsync()&lt;/code&gt; checks the current status without prompting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Android &lt;code dir=&quot;auto&quot;&gt;AndroidManifest.xml&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;uses-permission&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;android:name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;android.permission.READ_MEDIA_AUDIO&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;uses-permission&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;android:name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;android.permission.READ_EXTERNAL_STORAGE&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                 &lt;/span&gt;&lt;span&gt;android:maxSdkVersion&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;32&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;iOS &lt;code dir=&quot;auto&quot;&gt;Info.plist&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;&gt;NSAppleMusicUsageDescription&amp;#x3C;/&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&gt;This app needs access to your music library to browse and play your music.&amp;#x3C;/&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;track-metadata&quot;&gt;Track Metadata&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Every track comes back as a &lt;code dir=&quot;auto&quot;&gt;MusicMetadata&lt;/code&gt; record:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MusicMetadata&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;span&gt;,           &lt;/span&gt;&lt;span&gt;// Platform-specific unique ID&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Artist&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Album&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Genre&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;TimeSpan&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Duration&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;AlbumArtUri&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// Android only — null on iOS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ContentUri&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// content:// (Android) or ipod-library:// (iOS)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;querying-tracks&quot;&gt;Querying Tracks&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Get everything&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tracks&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; library.&lt;/span&gt;&lt;span&gt;GetAllTracksAsync&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Search across title, artist, and album&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;results&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; library.&lt;/span&gt;&lt;span&gt;SearchTracksAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Beatles&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;On Android, queries go through &lt;code dir=&quot;auto&quot;&gt;MediaStore.Audio.Media&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;ContentResolver&lt;/code&gt;.  On iOS, they use &lt;code dir=&quot;auto&quot;&gt;MPMediaQuery&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;MPMediaPropertyPredicate&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;file-export&quot;&gt;File Export&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Need to copy a track to your app’s storage?&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;destination&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Path.&lt;/span&gt;&lt;span&gt;Combine&lt;/span&gt;&lt;span&gt;(FileSystem.AppDataDirectory, &lt;/span&gt;&lt;span&gt;&quot;exported.m4a&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; library.&lt;/span&gt;&lt;span&gt;CopyTrackAsync&lt;/span&gt;&lt;span&gt;(track, destination);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Returns &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; if the copy isn’t possible — which brings us to the DRM caveat.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-music-player&quot;&gt;The Music Player&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;playback-controls&quot;&gt;Playback Controls&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; player.&lt;/span&gt;&lt;span&gt;PlayAsync&lt;/span&gt;&lt;span&gt;(track);  &lt;/span&gt;&lt;span&gt;// Load and play&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Pause&lt;/span&gt;&lt;span&gt;();                  &lt;/span&gt;&lt;span&gt;// Pause at current position&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Resume&lt;/span&gt;&lt;span&gt;();                 &lt;/span&gt;&lt;span&gt;// Resume from paused position&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Seek&lt;/span&gt;&lt;span&gt;(TimeSpan.&lt;/span&gt;&lt;span&gt;FromMinutes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;));  &lt;/span&gt;&lt;span&gt;// Jump to position&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;player.&lt;/span&gt;&lt;span&gt;Stop&lt;/span&gt;&lt;span&gt;();                   &lt;/span&gt;&lt;span&gt;// Stop and release&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;state-tracking&quot;&gt;State Tracking&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;PlaybackState&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; player.State;       &lt;/span&gt;&lt;span&gt;// Stopped, Playing, or Paused&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MusicMetadata&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; player.CurrentTrack;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TimeSpan&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; player.Position;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TimeSpan&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;duration&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; player.Duration;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;player.StateChanged &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;sender&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// React to state transitions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;player.PlaybackCompleted &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;sender&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// Track finished — load next?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;On Android, playback uses &lt;code dir=&quot;auto&quot;&gt;Android.Media.MediaPlayer&lt;/code&gt;.  On iOS, it uses &lt;code dir=&quot;auto&quot;&gt;AVAudioPlayer&lt;/code&gt; via AVFoundation.  Both are properly managed — resources are released on stop and disposal.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-drm-caveat&quot;&gt;The DRM Caveat&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is important to know upfront.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On iOS&lt;/strong&gt;, Apple Music subscription tracks (DRM-protected) &lt;strong&gt;cannot be played or copied&lt;/strong&gt; through this API.  For these tracks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;ContentUri&lt;/code&gt; will be an empty string&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;CopyTrackAsync&lt;/code&gt; returns &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;PlayAsync&lt;/code&gt; throws &lt;code dir=&quot;auto&quot;&gt;InvalidOperationException&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only locally synced or purchased (non-DRM) tracks work.  The metadata (title, artist, album, duration) is still available for all tracks — you just can’t access the audio data for DRM-protected ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On Android&lt;/strong&gt;, all locally stored music files work without restrictions via &lt;code dir=&quot;auto&quot;&gt;ContentResolver&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is an OS-level limitation, not a library limitation.  iOS simply does not expose the audio asset URL for DRM-protected media items.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;platform-support&quot;&gt;Platform Support&lt;/h2&gt;&lt;/div&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Platform&lt;/th&gt;&lt;th&gt;Minimum Version&lt;/th&gt;&lt;th&gt;Audio Query&lt;/th&gt;&lt;th&gt;Playback Engine&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Android&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;API 24 (Android 7.0)&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MediaStore.Audio.Media&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;Android.Media.MediaPlayer&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;15.0&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;MPMediaQuery&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;AVAudioPlayer&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Both platforms require a &lt;strong&gt;physical device&lt;/strong&gt; for meaningful testing — simulators and emulators don’t have music content.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;when-to-use-it&quot;&gt;When to Use It&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Music player apps that browse the device library&lt;/li&gt;
&lt;li&gt;Apps that need to search or display the user’s music collection&lt;/li&gt;
&lt;li&gt;Exporting audio files for processing (transcription, analysis, sharing)&lt;/li&gt;
&lt;li&gt;Any app that integrates with locally stored music&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not the best fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Streaming music from web sources (use &lt;code dir=&quot;auto&quot;&gt;MediaElement&lt;/code&gt; or a streaming library)&lt;/li&gt;
&lt;li&gt;Audio recording (use &lt;a href=&quot;https://github.com/jfversluis/Plugin.Maui.Audio&quot;&gt;Plugin.Maui.Audio&lt;/a&gt; or platform audio APIs)&lt;/li&gt;
&lt;li&gt;Background audio playback with lock screen controls (this is foreground playback)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Music&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Full source and sample app at the &lt;a href=&quot;https://github.com/shinyorg/music&quot;&gt;GitHub repository&lt;/a&gt;.  Documentation coming soon at &lt;a href=&quot;https://shinylib.net/client/music&quot;&gt;shinylib.net/client/music&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Music</category></item><item><title>Introducing Shiny.Spatial — A Dependency-Free Spatial Database and GPS Geofencing for .NET</title><link>https://www.shinylib.net/blog/2026/03/shiny-spatial/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/03/shiny-spatial/</guid><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’ve ever tried to do geospatial work on .NET MAUI, you know the options aren’t great.  SpatiaLite requires native binaries per platform.  NetTopologySuite is a full-featured geometry library — great on the server, but heavy on mobile and hostile to AOT.  And the built-in platform geofencing?  iOS gives you 20 circular regions.  Android gives you 60.  That’s it.&lt;/p&gt;
&lt;p&gt;I needed something different — a spatial database that works everywhere .NET runs, with zero native dependencies, and a geofencing system that can monitor thousands of polygon regions from a real geographic database.  So I built &lt;a href=&quot;https://github.com/shinyorg/geospatialdb&quot;&gt;Shiny.Spatial&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Two NuGet packages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Shiny.Spatial&lt;/code&gt;&lt;/strong&gt; — a spatial database engine that stores geometry in SQLite using R*Tree virtual tables, with all spatial algorithms implemented in pure C#.  No SpatiaLite, no NetTopologySuite, no native binaries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Shiny.Spatial.Geofencing&lt;/code&gt;&lt;/strong&gt; — a GPS-driven geofence monitor that watches spatial database tables for region entry/exit instead of registering individual fences with the OS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The spatial database targets &lt;code dir=&quot;auto&quot;&gt;netstandard2.0&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;net10.0&lt;/code&gt;, so it runs on MAUI, Blazor, console apps, servers — anywhere SQLite runs.  The geofencing package targets iOS and Android via MAUI.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-spatial-database&quot;&gt;The Spatial Database&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;two-pass-query-pipeline&quot;&gt;Two-Pass Query Pipeline&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Every spatial query follows the same pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pass 1 — R*Tree bounding box filter (SQL, O(log n)).&lt;/strong&gt;  SQLite’s R*Tree virtual table eliminates most candidates based on bounding box overlap.  This runs entirely in SQL and is extremely fast.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pass 2 — C# geometry refinement.&lt;/strong&gt;  Surviving candidates are tested with exact geometric predicates — point-in-polygon (ray-casting), segment intersection (cross-product), Haversine distance — in pure C#.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This two-pass approach is the same strategy PostGIS and SpatiaLite use internally.  The difference is that both passes here require zero native extensions.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// What happens under the hood for an Intersecting query:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Pass 1: SELECT * FROM cities_rtree WHERE min_x &amp;#x3C;= @maxX AND max_x &gt;= @minX ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Pass 2: SpatialPredicates.Intersects(candidate.Geometry, queryGeometry)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;creating-a-database&quot;&gt;Creating a Database&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SpatialDatabase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;locations.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;CreateTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;cities&quot;&lt;/span&gt;&lt;span&gt;, CoordinateSystem.Wgs84,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PropertyDefinition&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;, PropertyType.Text),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PropertyDefinition&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;population&quot;&lt;/span&gt;&lt;span&gt;, PropertyType.Integer)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Each table gets a single R*Tree virtual table with auxiliary columns for WKB-encoded geometry and user-defined properties.  No separate tables, no JOINs.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;geometry-types&quot;&gt;Geometry Types&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;All geometry classes are immutable and sealed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Point&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;LineString&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Polygon&lt;/code&gt; (with holes)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;MultiPoint&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;MultiLineString&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;MultiPolygon&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;GeometryCollection&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Coordinates use &lt;code dir=&quot;auto&quot;&gt;double X&lt;/code&gt; (longitude) and &lt;code dir=&quot;auto&quot;&gt;double Y&lt;/code&gt; (latitude).  &lt;code dir=&quot;auto&quot;&gt;Polygon&lt;/code&gt; supports interior rings (holes) — useful for real geographic boundaries like city limits with excluded areas.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// A simple polygon — Colorado&apos;s approximate boundary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;colorado&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Polygon&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;109.05&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;37.0&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;109.05&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;41.0&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;102.05&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;41.0&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;102.05&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;37.0&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;109.05&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;37.0&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;// closed ring&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;inserting-data&quot;&gt;Inserting Data&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Single insert&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;feature&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SpatialFeature&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Point&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;104.99&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;39.74&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Dictionary&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;?&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Denver&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;population&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;715522&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;table.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(feature);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Bulk insert — transaction-wrapped&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;table.&lt;/span&gt;&lt;span&gt;BulkInsert&lt;/span&gt;&lt;span&gt;(features);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;querying--the-fluent-builder&quot;&gt;Querying — The Fluent Builder&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;table.Query()&lt;/code&gt; returns a fluent builder that chains spatial filters, property filters, distance ordering, and paging:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Find all cities within 50km of Denver, ordered by distance&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nearby&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WithinDistance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;104.99&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;39.74&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;50_000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OrderByDistance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Coordinate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;104.99&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;39.74&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Limit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Find features inside a polygon&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;inColorado&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Intersecting&lt;/span&gt;&lt;span&gt;(coloradoBoundary)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Combine spatial + property filters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;largeCities&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WithinDistance&lt;/span&gt;&lt;span&gt;(center, &lt;/span&gt;&lt;span&gt;100_000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WhereProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;population&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;100000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OrderByDistance&lt;/span&gt;&lt;span&gt;(center)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Limit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Paging&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WhereProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;state&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;=&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;Colorado&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Limit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Offset&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Property filters run in SQL (Pass 1), spatial filters run as C# refinement (Pass 2), and distance ordering plus limit/offset are applied after refinement.  The query builder composes them automatically.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;direct-query-methods&quot;&gt;Direct Query Methods&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;For simple cases, skip the builder:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;feature&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;GetById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;42&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;inBox&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;FindInEnvelope&lt;/span&gt;&lt;span&gt;(envelope);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;intersecting&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;FindIntersecting&lt;/span&gt;&lt;span&gt;(polygon);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;contained&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;FindContainedBy&lt;/span&gt;&lt;span&gt;(polygon);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nearby&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;FindWithinDistance&lt;/span&gt;&lt;span&gt;(center, &lt;/span&gt;&lt;span&gt;10_000&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// meters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;All benchmarks on Apple M2, .NET 10, in-memory SQLite, 100K point features:&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Query&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;FindIntersecting (polygon)&lt;/td&gt;&lt;td&gt;1.15 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FindWithinDistance&lt;/td&gt;&lt;td&gt;183 us&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FindContainedBy&lt;/td&gt;&lt;td&gt;987 us&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GetById&lt;/td&gt;&lt;td&gt;9.4 us&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fluent: spatial + property filter&lt;/td&gt;&lt;td&gt;1.44 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fluent: distance + order + limit&lt;/td&gt;&lt;td&gt;254 us&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Insert&lt;/th&gt;&lt;th&gt;Count&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;BulkInsert&lt;/td&gt;&lt;td&gt;1,000&lt;/td&gt;&lt;td&gt;9.8 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;BulkInsert&lt;/td&gt;&lt;td&gt;10,000&lt;/td&gt;&lt;td&gt;93 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;BulkInsert&lt;/td&gt;&lt;td&gt;100,000&lt;/td&gt;&lt;td&gt;964 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The pure algorithms are fast too — Haversine at 28ns, point-in-polygon (5 vertices) at 24ns, segment intersection at 3ns.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;serialization&quot;&gt;Serialization&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Geometry is stored as WKB (Well-Known Binary), the same binary format used by PostGIS, SpatiaLite, and every major GIS tool.  The built-in &lt;code dir=&quot;auto&quot;&gt;WkbReader&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;WkbWriter&lt;/code&gt; handle all seven geometry types.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;gps-geofencing&quot;&gt;GPS Geofencing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is where it gets interesting for MAUI developers.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-problem-with-platform-geofencing&quot;&gt;The Problem with Platform Geofencing&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Native geofencing on mobile has hard limits:&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;iOS&lt;/th&gt;&lt;th&gt;Android&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Max regions&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;td&gt;60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Shape&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Circle only&lt;/td&gt;&lt;td&gt;Circle only&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Precision&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;OS-determined&lt;/td&gt;&lt;td&gt;OS-determined&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Timing&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;OS-determined&lt;/td&gt;&lt;td&gt;OS-determined&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Handler time&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;~4 seconds&lt;/td&gt;&lt;td&gt;Limited&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Emulator testing&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Works&lt;/td&gt;&lt;td&gt;Unreliable&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If your app needs to know when a user enters Denver, you register a circular region around Denver’s center.  But Denver isn’t a circle — it’s an irregular polygon.  And if your app needs to track entry/exit for 200 cities, you’re out of luck.  You’d need 200 regions, but the OS caps you at 20 or 60.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;how-shinyspatialgeofencing-works&quot;&gt;How Shiny.Spatial.Geofencing Works&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Instead of registering individual fences with the OS, &lt;code dir=&quot;auto&quot;&gt;Shiny.Spatial.Geofencing&lt;/code&gt; hooks into GPS updates and queries your spatial databases in real-time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPS reading comes in&lt;/li&gt;
&lt;li&gt;For each monitored table, query &lt;code dir=&quot;auto&quot;&gt;table.Query().Intersecting(point).FirstOrDefault()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Compare the current feature ID with the previously stored feature ID&lt;/li&gt;
&lt;li&gt;If they differ — fire exit for the old region and enter for the new one&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No OS region limits.  No circular approximations.  Real polygon boundaries from real geographic data.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Install the package:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Spatial.Geofencing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Create a delegate to handle region changes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyGeofenceDelegate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ILogger&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyGeofenceDelegate&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;logger&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;INotificationManager&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;notifications&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;ISpatialGeofenceDelegate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OnRegionChanged&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SpatialRegionChange&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;change&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; change.Region.Properties.&lt;/span&gt;&lt;span&gt;GetValueOrDefault&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Unknown&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; change.Entered &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Entered&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Exited&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;logger.&lt;/span&gt;&lt;span&gt;LogInformation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;{Action} {Region} (table: {Table})&quot;&lt;/span&gt;&lt;span&gt;, action, name, change.TableName);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; notifications.&lt;/span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Geofence&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;$&quot;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Register in &lt;code dir=&quot;auto&quot;&gt;MauiProgram.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddSpatialGps&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyGeofenceDelegate&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; cfg&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(dbPath, &lt;/span&gt;&lt;span&gt;&quot;states&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(dbPath, &lt;/span&gt;&lt;span&gt;&quot;cities&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it.  The library now monitors both the &lt;code dir=&quot;auto&quot;&gt;states&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;cities&lt;/code&gt; tables.  When the user crosses a state boundary, you get an exit + enter event.  When they enter a city, you get a separate enter event.  The two layers are tracked independently.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-api&quot;&gt;The API&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ISpatialGeofenceManager&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IsStarted&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AccessState&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;RequestAccess&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Start&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Stop&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;IReadOnlyList&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SpatialCurrentRegion&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;GetCurrent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cancelToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;GetCurrent()&lt;/code&gt; takes a one-shot GPS reading and tells you which region the device is currently in for each monitored table — useful for initial state on app launch.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;configuration&quot;&gt;Configuration&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddSpatialGps&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyGeofenceDelegate&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cfg.MinimumDistance &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Distance.&lt;/span&gt;&lt;span&gt;FromMeters&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;);  &lt;/span&gt;&lt;span&gt;// default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cfg.MinimumTime &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; TimeSpan.&lt;/span&gt;&lt;span&gt;FromMinutes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);       &lt;/span&gt;&lt;span&gt;// default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cfg.&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(statesDbPath, &lt;/span&gt;&lt;span&gt;&quot;states&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cfg.&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(citiesDbPath, &lt;/span&gt;&lt;span&gt;&quot;cities&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;MinimumDistance&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;MinimumTime&lt;/code&gt; control how frequently GPS readings trigger spatial queries.  The defaults balance battery life with responsiveness.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;multi-layer-monitoring&quot;&gt;Multi-Layer Monitoring&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Because each table is tracked independently, you get precise layer-specific events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drive from Denver suburbs into Denver city limits → &lt;code dir=&quot;auto&quot;&gt;Entered: Denver&lt;/code&gt; (cities table)&lt;/li&gt;
&lt;li&gt;Drive from Denver to Colorado Springs → &lt;code dir=&quot;auto&quot;&gt;Exited: Denver&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Entered: Colorado Springs&lt;/code&gt; (cities table) — no state change event&lt;/li&gt;
&lt;li&gt;Drive from Colorado into Kansas → &lt;code dir=&quot;auto&quot;&gt;Exited: Colorado&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Entered: Kansas&lt;/code&gt; (states table) — plus whatever city events apply&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;pre-built-databases&quot;&gt;Pre-Built Databases&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The repo ships with ready-to-use databases:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Database&lt;/th&gt;&lt;th&gt;Table&lt;/th&gt;&lt;th&gt;Geometry&lt;/th&gt;&lt;th&gt;Records&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;us-states.db&lt;/code&gt;&lt;/td&gt;&lt;td&gt;states&lt;/td&gt;&lt;td&gt;Polygon&lt;/td&gt;&lt;td&gt;51&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;us-cities.db&lt;/code&gt;&lt;/td&gt;&lt;td&gt;cities&lt;/td&gt;&lt;td&gt;Point&lt;/td&gt;&lt;td&gt;100&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;ca-provinces.db&lt;/code&gt;&lt;/td&gt;&lt;td&gt;provinces&lt;/td&gt;&lt;td&gt;Polygon&lt;/td&gt;&lt;td&gt;13&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;ca-cities.db&lt;/code&gt;&lt;/td&gt;&lt;td&gt;cities&lt;/td&gt;&lt;td&gt;Point&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Include them as MAUI assets and copy to app data on first launch.  Or build your own from any GeoJSON source using the included &lt;code dir=&quot;auto&quot;&gt;DatabaseSeeder&lt;/code&gt; tool.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-not-spatialite-or-nettopologysuite&quot;&gt;Why Not SpatiaLite or NetTopologySuite?&lt;/h2&gt;&lt;/div&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Concern&lt;/th&gt;&lt;th&gt;SpatiaLite&lt;/th&gt;&lt;th&gt;NetTopologySuite&lt;/th&gt;&lt;th&gt;Shiny.Spatial&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Native binaries&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Yes — per platform&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;AOT compatible&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Platform-dependent&lt;/td&gt;&lt;td&gt;Reflection-heavy&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Trimmable&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bundle size&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Large (native libs)&lt;/td&gt;&lt;td&gt;Moderate&lt;/td&gt;&lt;td&gt;Small (single dep: Microsoft.Data.Sqlite)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Geometry algorithms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;C/C++&lt;/td&gt;&lt;td&gt;C#&lt;/td&gt;&lt;td&gt;C#&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;R*Tree indexing&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Built-in&lt;/td&gt;&lt;td&gt;Separate&lt;/td&gt;&lt;td&gt;Built-in (SQLite)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MAUI-ready&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Requires native binding per platform&lt;/td&gt;&lt;td&gt;Works but AOT issues&lt;/td&gt;&lt;td&gt;Works everywhere&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The tradeoff is coverage.  SpatiaLite and NTS support hundreds of spatial operations — buffer, union, difference, convex hull, spatial joins.  Shiny.Spatial covers the operations that matter for mobile apps: intersects, contains, within-distance, nearest-to.  If you need computational geometry operations, use NTS on the server and ship the results as a spatial database to the device.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;when-to-use-it&quot;&gt;When to Use It&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Geofencing beyond OS limits (more than 20/60 regions, polygon shapes)&lt;/li&gt;
&lt;li&gt;Location-aware apps that need “which city/state/zone am I in?”&lt;/li&gt;
&lt;li&gt;Spatial search (find nearby points of interest, stores, landmarks)&lt;/li&gt;
&lt;li&gt;Offline spatial queries without a server round-trip&lt;/li&gt;
&lt;li&gt;Shipping pre-built geographic databases with your app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not the best fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple circular geofencing with a few regions (use &lt;code dir=&quot;auto&quot;&gt;Shiny.Locations&lt;/code&gt; IGeofenceManager instead)&lt;/li&gt;
&lt;li&gt;Heavy computational geometry (buffer, union, difference) — use NTS on the server&lt;/li&gt;
&lt;li&gt;Real-time map rendering — this is a query engine, not a rendering engine&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Spatial&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Spatial.Geofencing&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# for MAUI GPS geofencing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Full documentation at &lt;a href=&quot;https://shinylib.net/spatial&quot;&gt;shinylib.net/spatial&lt;/a&gt; and the &lt;a href=&quot;https://github.com/shinyorg/geospatialdb&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Spatial</category></item><item><title>Introducing Shiny.SqliteDocumentDb — Schema-Free JSON Documents in SQLite</title><link>https://www.shinylib.net/blog/2026/02/shiny-sqlitedocumentdb/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/02/shiny-sqlitedocumentdb/</guid><pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been building .NET apps long enough to know that SQLite is the workhorse of local storage.  It’s everywhere — mobile apps, desktop apps, embedded systems, even server-side caches.  But every time I reach for sqlite-net or raw ADO.NET, I end up in the same loop: design tables, write migrations, manage foreign keys, rehydrate object graphs from JOINs.&lt;/p&gt;
&lt;p&gt;For a lot of use cases — settings stores, offline caches, app state, anything with nested data — that ceremony is overkill.  What I actually want is to throw an object in and get it back out.  So I built &lt;a href=&quot;https://github.com/shinyorg/SqliteDocumentDb&quot;&gt;Shiny.SqliteDocumentDb&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;It’s a lightweight document store that sits on top of SQLite.  You give it a .NET object, it serializes it to JSON and stores it.  You query it with a fluent LINQ query builder, and it translates those expressions to &lt;code dir=&quot;auto&quot;&gt;json_extract&lt;/code&gt; SQL under the hood.  No schema, no migrations, no table design.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Store a document — Id is auto-generated for Guid/int/long types&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; { Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;alice-1&quot;&lt;/span&gt;&lt;span&gt;, Name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;, Age &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;25&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(user);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Fluent query builder&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;results&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Name &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OrderBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Every document type must have a public &lt;code dir=&quot;auto&quot;&gt;Id&lt;/code&gt; property of type &lt;code dir=&quot;auto&quot;&gt;Guid&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;int&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;long&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;string&lt;/code&gt;. The Id is stored in both the SQLite column and the JSON blob, so query results always include it.&lt;/p&gt;
&lt;p&gt;All &lt;code dir=&quot;auto&quot;&gt;JsonTypeInfo&amp;#x3C;T&gt;&lt;/code&gt; parameters are optional — configure a &lt;code dir=&quot;auto&quot;&gt;JsonSerializerContext&lt;/code&gt; once and type info is auto-resolved on every call.  No per-call &lt;code dir=&quot;auto&quot;&gt;ctx.User&lt;/code&gt; needed.  Full AOT, fully trimmable, zero reflection.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-not-just-use-sqlite-net&quot;&gt;Why Not Just Use sqlite-net?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;sqlite-net is great for flat, single-table CRUD.  But the moment your data has structure — an order with line items, a user with addresses, a config with nested sections — things get painful.  You need multiple tables, foreign keys, multiple inserts per save, and multiple queries plus manual rehydration per read.&lt;/p&gt;
&lt;p&gt;The document store approach collapses all of that into a single operation.  One write, one read, one document.&lt;/p&gt;
&lt;p&gt;The benchmarks tell the story:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nested insert (Order + Address + OrderLines + Tags, 100 records):&lt;/strong&gt;&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DocumentStore&lt;/td&gt;&lt;td&gt;5.69 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;sqlite-net (3 tables)&lt;/td&gt;&lt;td&gt;176.48 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Nested get by ID:&lt;/strong&gt;&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DocumentStore&lt;/td&gt;&lt;td&gt;5.04 us&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;sqlite-net (3 queries)&lt;/td&gt;&lt;td&gt;48.26 us&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;That’s &lt;strong&gt;31x faster inserts&lt;/strong&gt; and &lt;strong&gt;10x faster reads&lt;/strong&gt; for nested data.  The document store wins because it does one write and one read instead of multiple table operations.&lt;/p&gt;
&lt;p&gt;For flat data, sqlite-net can be faster on indexed column queries (it queries columns directly vs. &lt;code dir=&quot;auto&quot;&gt;json_extract&lt;/code&gt;).  Use the right tool for the shape of your data.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-about-ef-core-on-maui&quot;&gt;What About EF Core on MAUI?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The other question I get is “why not just use EF Core?”  On a server, EF Core is a reasonable choice.  On .NET MAUI — iOS, Android, Mac Catalyst — it becomes a liability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AOT is not optional on Apple platforms.&lt;/strong&gt;  iOS, iPadOS, tvOS, and Mac Catalyst all prohibit JIT compilation at the OS level.  EF Core relies heavily on runtime reflection and dynamic code generation for change tracking, query compilation, and model building.  Its public API is decorated with &lt;code dir=&quot;auto&quot;&gt;[RequiresDynamicCode]&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;[RequiresUnreferencedCode]&lt;/code&gt; throughout.  That’s a non-starter for fully native AOT deployments on Apple platforms.&lt;/p&gt;
&lt;p&gt;Android doesn’t prohibit JIT, but AOT (&lt;code dir=&quot;auto&quot;&gt;PublishAot&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;AndroidEnableProfiledAot&lt;/code&gt;) delivers measurably faster startup and lower memory usage — both of which directly affect user experience on mobile.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Migrations solve a problem mobile apps don’t have.&lt;/strong&gt;  On a server, you run migrations against a shared database with a known lifecycle.  On a mobile device, the database is created on first launch or ships inside the app bundle.  EF Core’s migration pipeline (&lt;code dir=&quot;auto&quot;&gt;Add-Migration&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Update-Database&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;__EFMigrationsHistory&lt;/code&gt;) adds complexity with no real benefit.  A schema-free document store eliminates migrations entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The dependency graph is heavy.&lt;/strong&gt;  EF Core pulls in &lt;code dir=&quot;auto&quot;&gt;Microsoft.EntityFrameworkCore&lt;/code&gt;, its SQLite provider, design-time packages, and their transitive dependencies.  That increases app bundle size — a real concern when app stores enforce download limits and users expect fast installs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mobile data is document-shaped.&lt;/strong&gt;  User preferences, cached API responses, offline data queues, local state — this data naturally has nested structure.  Forcing it into normalized tables with foreign keys and JOINs adds accidental complexity.&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Concern&lt;/th&gt;&lt;th&gt;EF Core&lt;/th&gt;&lt;th&gt;Shiny.SqliteDocumentDb&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;AOT / trimming&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Reflection-heavy; no AOT support&lt;/td&gt;&lt;td&gt;Optional &lt;code dir=&quot;auto&quot;&gt;JsonTypeInfo&amp;#x3C;T&gt;&lt;/code&gt; on every API; auto-resolves from context&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Migrations&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Required for every schema change&lt;/td&gt;&lt;td&gt;Not needed — schema-free JSON&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nested objects&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Normalized tables, foreign keys, JOINs&lt;/td&gt;&lt;td&gt;Single document, single write, single read&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;App bundle size&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Large dependency tree&lt;/td&gt;&lt;td&gt;Single dependency on &lt;code dir=&quot;auto&quot;&gt;Microsoft.Data.Sqlite&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Startup time&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;DbContext model building, migration checks&lt;/td&gt;&lt;td&gt;Open connection and go&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The .NET trimmer makes this worse.  Libraries that depend on reflection break under trimming because the trimmer can’t statically determine which types and members are accessed at runtime.  This forces you to either disable trimming (larger binaries) or maintain complex trimmer XML configuration.  This library avoids both problems — source-generated JSON serialization means the trimmer can see every type, and there’s no &lt;code dir=&quot;auto&quot;&gt;Expression.Compile()&lt;/code&gt;, no &lt;code dir=&quot;auto&quot;&gt;Reflection.Emit&lt;/code&gt;, no dynamic delegates anywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-fluent-query-builder&quot;&gt;The Fluent Query Builder&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the heart of the API.  &lt;code dir=&quot;auto&quot;&gt;store.Query&amp;#x3C;T&gt;()&lt;/code&gt; returns a fluent builder where you chain &lt;code dir=&quot;auto&quot;&gt;.Where()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.OrderBy()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.Paginate()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.Select()&lt;/code&gt; — then terminate with &lt;code dir=&quot;auto&quot;&gt;.ToList()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.ToAsyncEnumerable()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.Count()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.Any()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.ExecuteDelete()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.ExecuteUpdate()&lt;/code&gt;, or any aggregate method.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Filter + sort + paginate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OrderBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Name)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Paginate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Count matching documents&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.Status &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Pending&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Count&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Delete by predicate — returns count deleted&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deleted&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ExecuteDelete&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Bulk update a property — returns count updated&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;updated&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ExecuteUpdate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age, &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Scalar aggregates&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;maxAge&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;().&lt;/span&gt;&lt;span&gt;Max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;avgAge&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.IsActive)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Average&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The expression visitor translates C# LINQ expressions into SQLite &lt;code dir=&quot;auto&quot;&gt;json_extract&lt;/code&gt; SQL, resolving property names from &lt;code dir=&quot;auto&quot;&gt;JsonTypeInfo&lt;/code&gt; metadata so &lt;code dir=&quot;auto&quot;&gt;[JsonPropertyName]&lt;/code&gt; and camelCase policies work correctly.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Nested properties&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;portland&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.ShippingAddress.City &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Portland&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → json_extract(Data, &apos;$.shippingAddress.city&apos;) = @p0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Collection queries with Any()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasWidgets&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.Lines.&lt;/span&gt;&lt;span&gt;Any&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;l&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; l.ProductName &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Widget&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → EXISTS (SELECT 1 FROM json_each(Data, &apos;$.lines&apos;) WHERE ...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Collection Count()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bigOrders&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.Lines.&lt;/span&gt;&lt;span&gt;Count&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → json_array_length(Data, &apos;$.lines&apos;) &gt; 5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// String methods&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Name.&lt;/span&gt;&lt;span&gt;Contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;li&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → LIKE &apos;%&apos; || @p0 || &apos;%&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Equality, comparisons, logical operators (&lt;code dir=&quot;auto&quot;&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;||&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;!&lt;/code&gt;), null checks, &lt;code dir=&quot;auto&quot;&gt;DateTime&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;DateTimeOffset&lt;/code&gt;, captured variables — they all work.  The full expression reference is in the &lt;a href=&quot;https://github.com/shinyorg/SqliteDocumentDb&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;sql-level-projections&quot;&gt;SQL-Level Projections&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Need just a few fields from a large document?  Chain &lt;code dir=&quot;auto&quot;&gt;.Select()&lt;/code&gt; to extract only the selected properties at the database level using &lt;code dir=&quot;auto&quot;&gt;json_object&lt;/code&gt; — no full deserialization.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;summaries&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.Status &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Shipped&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OrderBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.CustomerName)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Paginate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Select&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OrderSummary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Customer &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; o.CustomerName,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;City &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; o.ShippingAddress.City,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;LineCount &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; o.Lines.&lt;/span&gt;&lt;span&gt;Count&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code dir=&quot;auto&quot;&gt;Lines.Count()&lt;/code&gt; becomes &lt;code dir=&quot;auto&quot;&gt;json_array_length(Data, &apos;$.lines&apos;)&lt;/code&gt; in SQL.  You can also use &lt;code dir=&quot;auto&quot;&gt;Any()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Any(predicate)&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Count(predicate)&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Sum()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Max()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Min()&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;Average()&lt;/code&gt; inside selectors.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;aggregate-projections&quot;&gt;Aggregate Projections&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For GROUP BY queries, use the &lt;code dir=&quot;auto&quot;&gt;Sql&lt;/code&gt; marker class inside &lt;code dir=&quot;auto&quot;&gt;.Select()&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stats&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.Status &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Cancelled&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Select&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OrderStats&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Status &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; o.Status,            &lt;/span&gt;&lt;span&gt;// GROUP BY column&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;OrderCount &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Sql.&lt;/span&gt;&lt;span&gt;Count&lt;/span&gt;&lt;span&gt;(),     &lt;/span&gt;&lt;span&gt;// COUNT(*)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TotalRevenue &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Sql.&lt;/span&gt;&lt;span&gt;Sum&lt;/span&gt;&lt;span&gt;(o.TotalAmount),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Non-aggregate columns are automatically grouped.  &lt;code dir=&quot;auto&quot;&gt;Sql.Count()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Sql.Max()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Sql.Min()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Sql.Sum()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Sql.Avg()&lt;/code&gt; — all available.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;surgical-field-updates&quot;&gt;Surgical Field Updates&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Don’t always need to replace an entire document.  &lt;code dir=&quot;auto&quot;&gt;SetProperty&lt;/code&gt; updates a single field in-place via &lt;code dir=&quot;auto&quot;&gt;json_set()&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;RemoveProperty&lt;/code&gt; strips a field via &lt;code dir=&quot;auto&quot;&gt;json_remove()&lt;/code&gt; — both without deserializing the document.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Update a single field&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;SetProperty&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;user-1&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age, &lt;/span&gt;&lt;span&gt;31&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Nested paths work too&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;SetProperty&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;order-1&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.ShippingAddress.City, &lt;/span&gt;&lt;span&gt;&quot;Portland&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Strip a field entirely&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;RemoveProperty&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;user-1&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Email);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For multi-field patches, &lt;code dir=&quot;auto&quot;&gt;Upsert&lt;/code&gt; does RFC 7396 JSON Merge Patch — deep-merging only the provided fields while preserving everything else:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Upsert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; { Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;user-1&quot;&lt;/span&gt;&lt;span&gt;, Name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;, Age &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;31&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Email and other fields are preserved&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;streaming-with-iasyncenumerable&quot;&gt;Streaming with IAsyncEnumerable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Use &lt;code dir=&quot;auto&quot;&gt;.ToAsyncEnumerable()&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;.ToList()&lt;/code&gt; to stream results one-at-a-time without buffering the entire set into memory.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;foreach&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.Status &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Pending&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OrderBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.CustomerName)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ToAsyncEnumerable&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ProcessOrder&lt;/span&gt;&lt;span&gt;(order);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The benchmarks show streaming eliminates Gen1 GC collections entirely at 1,000+ documents while maintaining within ~2% of buffered throughput.  If you’re processing results incrementally, streaming is free performance.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;expression-based-json-indexes&quot;&gt;Expression-Based JSON Indexes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The default query performance is solid, but for hot paths you can create indexes on &lt;code dir=&quot;auto&quot;&gt;json_extract&lt;/code&gt; expressions:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;CreateIndexAsync&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Name);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// CREATE INDEX IF NOT EXISTS idx_json_User_name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// ON documents (json_extract(Data, &apos;$.name&apos;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// WHERE TypeName = &apos;User&apos;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Impact on a 1,000-record flat query:&lt;/strong&gt;&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Mean&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Without index&lt;/td&gt;&lt;td&gt;270 us&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;With index&lt;/td&gt;&lt;td&gt;8.52 us&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;~32x faster.&lt;/strong&gt;  The index lets SQLite use a B-tree lookup instead of scanning every row with &lt;code dir=&quot;auto&quot;&gt;json_extract&lt;/code&gt;.  Works with nested properties too:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;CreateIndexAsync&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; o.ShippingAddress.City);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;aot-and-trimming--first-class&quot;&gt;AOT and Trimming — First Class&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;All &lt;code dir=&quot;auto&quot;&gt;JsonTypeInfo&amp;#x3C;T&gt;&lt;/code&gt; parameters are optional with &lt;code dir=&quot;auto&quot;&gt;= null&lt;/code&gt; defaults.  Configure a &lt;code dir=&quot;auto&quot;&gt;JsonSerializerContext&lt;/code&gt; once at setup and every method auto-resolves type info — no per-call parameters needed.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;JsonSerializable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;))]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;JsonSerializable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;))]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AppJsonContext&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;JsonSerializerContext&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AppJsonContext&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JsonSerializerOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;PropertyNamingPolicy &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; JsonNamingPolicy.CamelCase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DocumentStoreOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JsonSerializerOptions &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ctx.Options,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;UseReflectionFallback &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// recommended for AOT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Now every call is AOT-safe without passing JsonTypeInfo explicitly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; { Id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;alice-1&quot;&lt;/span&gt;&lt;span&gt;, Name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;, Age &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;25&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(user);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;().&lt;/span&gt;&lt;span&gt;Where&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; u.Age &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;18&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;ToList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Pass &lt;code dir=&quot;auto&quot;&gt;ctx.Options&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;DocumentStoreOptions.JsonSerializerOptions&lt;/code&gt; so the expression visitor and serializer share the same naming configuration.  That’s the one thing people forget — and then their LINQ queries silently return zero results because property names don’t match.&lt;/p&gt;
&lt;p&gt;Set &lt;code dir=&quot;auto&quot;&gt;UseReflectionFallback = false&lt;/code&gt; for AOT deployments.  Instead of opaque runtime failures, you get a clear &lt;code dir=&quot;auto&quot;&gt;InvalidOperationException&lt;/code&gt; telling you exactly which type is missing from your &lt;code dir=&quot;auto&quot;&gt;JsonSerializerContext&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;transactions&quot;&gt;Transactions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Atomic multi-document operations with automatic commit/rollback:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; store.&lt;/span&gt;&lt;span&gt;RunInTransaction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; tx.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(order); &lt;/span&gt;&lt;span&gt;// order.Id must be set&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; tx.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(user);  &lt;/span&gt;&lt;span&gt;// user.Id must be set&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// Exception → automatic rollback&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;tx&lt;/code&gt; parameter is a full &lt;code dir=&quot;auto&quot;&gt;IDocumentStore&lt;/code&gt;, so you can use any operation inside the transaction — queries, counts, removes, everything.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;di-registration&quot;&gt;DI Registration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One line:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Registers &lt;code dir=&quot;auto&quot;&gt;IDocumentStore&lt;/code&gt; as a singleton.  For full configuration:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddSqliteDocumentStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;opts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.ConnectionString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Data Source=mydata.db&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.TypeNameResolution &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; TypeNameResolution.FullName;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.JsonSerializerOptions &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ctx.Options;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;opts.UseReflectionFallback &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;when-to-use-it&quot;&gt;When to Use It&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offline caches and app state&lt;/li&gt;
&lt;li&gt;Settings and configuration stores&lt;/li&gt;
&lt;li&gt;Data with nested objects and child collections&lt;/li&gt;
&lt;li&gt;Rapid prototyping without schema design&lt;/li&gt;
&lt;li&gt;Any scenario where you want to store and query object graphs without table design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not the best fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bulk operations on millions of rows where raw SQL shines&lt;/li&gt;
&lt;li&gt;Simple flat-table CRUD where sqlite-net is already working well&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.SqliteDocumentDb&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Full documentation at &lt;a href=&quot;https://shinylib.net/sqlite-docdb/&quot;&gt;shinylib.net/sqlite-docdb&lt;/a&gt; and the &lt;a href=&quot;https://github.com/shinyorg/SqliteDocumentDb&quot;&gt;GitHub repository&lt;/a&gt; has the complete README with benchmarks, expression reference tables, and examples.&lt;/p&gt;</content:encoded><category>Extensions</category></item><item><title>Introducing Shiny.Maui.TableView — Settings-Style Pages for .NET MAUI, Without the Platform Pain</title><link>https://www.shinylib.net/blog/2026/02/shiny-maui-tableview/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/02/shiny-maui-tableview/</guid><pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;a href=&quot;https://www.nuget.org/packages/Shiny.Maui.TableView&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://img.shields.io/nuget/v/Shiny.Maui.TableView?style=for-the-badge&amp;#x26;logo=nuget&amp;#x26;label=Shiny.Maui.TableView&quot; alt=&quot;NuGet package Shiny.Maui.TableView&quot;&gt;&lt;/a&gt;
&lt;p&gt;If you’ve built a .NET MAUI app, you’ve probably needed a settings page.  A scrollable list of sections with toggles, text entries, pickers, radio buttons — the kind of UI you see in every iOS Settings screen or Android preferences panel.  And if you’ve tried to build one, you know it’s surprisingly painful.&lt;/p&gt;
&lt;p&gt;MAUI’s built-in &lt;code dir=&quot;auto&quot;&gt;TableView&lt;/code&gt; is limited.  The community options either depend on native renderers that break across platform updates or haven’t kept up with modern .NET.  I wanted something that just worked — pure MAUI, no platform-specific code, full MVVM support, and enough cell types to cover real app scenarios without writing custom templates for everything.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href=&quot;https://github.com/shinyorg/shinytableview&quot;&gt;Shiny.Maui.TableView&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;It’s a settings-style TableView control built entirely on .NET MAUI layout primitives.  No custom handlers, no native renderers, no platform-specific code.  It ships with 15 cell types that cover the most common settings UI patterns, a three-level cascading style system, drag-and-drop reordering, dynamic section generation, and full MVVM data binding.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;xmlns:tv=&quot;http://shiny.net/maui/tableview&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CellAccentColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#007AFF&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableRoot&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Network&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:SwitchCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Wi-Fi&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                           &lt;/span&gt;&lt;span&gt;On&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding WifiEnabled, Mode=TwoWay}&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:SwitchCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Bluetooth&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                           &lt;/span&gt;&lt;span&gt;On&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding BluetoothEnabled, Mode=TwoWay}&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableRoot&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s a fully functional settings section with two-way bound toggles.  No custom renderers, no platform init code beyond one line in &lt;code dir=&quot;auto&quot;&gt;MauiProgram.cs&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-pure-maui&quot;&gt;Why Pure MAUI?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The existing community solution — AiForms.Maui.SettingsView — is excellent, but it uses native platform renderers (&lt;code dir=&quot;auto&quot;&gt;UITableView&lt;/code&gt; on iOS, &lt;code dir=&quot;auto&quot;&gt;RecyclerView&lt;/code&gt; on Android).  That means platform-specific bugs, maintenance burden across OS updates, and behavior differences between platforms.&lt;/p&gt;
&lt;p&gt;Shiny.Maui.TableView takes a different approach: everything is built from &lt;code dir=&quot;auto&quot;&gt;ContentView&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;ScrollView&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;VerticalStackLayout&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Grid&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;Border&lt;/code&gt;.  One codebase, identical behavior everywhere.  It runs on iOS, Android, and Mac Catalyst with zero platform-specific code.&lt;/p&gt;
&lt;p&gt;The tradeoff is no virtualization — this is a full-render model.  For settings pages with dozens of items, that’s perfectly fine.  This isn’t meant for scrolling through thousands of rows; it’s meant for building the kind of structured, section-based UI that settings and forms demand.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;15-cell-types&quot;&gt;15 Cell Types&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The library ships with cell types that cover the breadth of settings UI patterns:&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;display--navigation&quot;&gt;Display &amp;#x26; Navigation&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LabelCell&lt;/strong&gt; — Read-only title/value display&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CommandCell&lt;/strong&gt; — Tappable cell with optional disclosure arrow and command binding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ButtonCell&lt;/strong&gt; — Full-width button-style action cell&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;toggles--selection&quot;&gt;Toggles &amp;#x26; Selection&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SwitchCell&lt;/strong&gt; — Toggle with customizable accent color&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CheckboxCell&lt;/strong&gt; — Native checkbox with accent color&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SimpleCheckCell&lt;/strong&gt; — Lightweight checkmark for selection lists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RadioCell&lt;/strong&gt; — Radio selection within a section or across the entire TableView&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;text-input&quot;&gt;Text Input&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EntryCell&lt;/strong&gt; — Inline text entry with placeholder, keyboard type, password masking, and max length&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;pickers&quot;&gt;Pickers&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DatePickerCell&lt;/strong&gt; — Native date picker dialog&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TimePickerCell&lt;/strong&gt; — Native time picker dialog&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TextPickerCell&lt;/strong&gt; — Dropdown-style picker from a list of items&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NumberPickerCell&lt;/strong&gt; — Numeric input via dialog with min/max/unit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PickerCell&lt;/strong&gt; — Full-page selection with single or multi-select, auto-generated display text, and configurable pick-to-close behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;custom&quot;&gt;Custom&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CustomCell&lt;/strong&gt; — Host any MAUI View with optional full-width mode, command binding, and disclosure arrow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every cell shares a common base with title, description, hint text, icon, background color, selection highlight, and border customization.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;cascading-styles&quot;&gt;Cascading Styles&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One of the features I’m most happy with is the three-level cascading style system.  Set defaults at the TableView level, override at the section level, and fine-tune on individual cells.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 1 — TableView (global defaults):&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CellTitleColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#333333&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CellTitleFontSize&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;17&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CellDescriptionColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#888888&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CellValueTextColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#007AFF&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CellBackgroundColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;White&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CellAccentColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#007AFF&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;HeaderTextColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#666666&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SeparatorColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#C6C6C8&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This sets the look for every cell and header in the entire TableView.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 2 — TableSection (section overrides):&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Important&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                 &lt;/span&gt;&lt;span&gt;HeaderBackgroundColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#E3F2FD&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                 &lt;/span&gt;&lt;span&gt;HeaderTextColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;#1565C0&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Level 3 — Individual cell (highest priority):&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:LabelCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Warning&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TitleColor&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Red&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The resolution logic is simple: cell property wins over section property, section wins over TableView, TableView wins over framework defaults.  You set your theme once at the top and only override where you need to.&lt;/p&gt;
&lt;p&gt;The style system covers everything: title, description, hint text, and value text fonts and colors; icon size and radius; accent color for interactive controls; cell backgrounds and selection highlights; header and footer appearance; separator color, height, and inset; section gap height and color; cell padding and borders.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;dynamic-sections&quot;&gt;Dynamic Sections&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Static XAML is great for fixed settings pages, but sometimes you need sections or cells generated from data.  Both &lt;code dir=&quot;auto&quot;&gt;TableView&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;TableSection&lt;/code&gt; support &lt;code dir=&quot;auto&quot;&gt;ItemsSource&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;DataTemplate&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dynamic cells within a section:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Devices&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                 &lt;/span&gt;&lt;span&gt;ItemsSource&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding Devices}&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection.ItemTemplate&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;DataTemplate&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:LabelCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding Name}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                          &lt;/span&gt;&lt;span&gt;ValueText&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding Status}&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;DataTemplate&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableSection.ItemTemplate&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can mix static and templated cells in the same section using &lt;code dir=&quot;auto&quot;&gt;TemplateStartIndex&lt;/code&gt; to control where generated cells appear.  The binding supports &lt;code dir=&quot;auto&quot;&gt;INotifyCollectionChanged&lt;/code&gt;, so adding or removing items from your collection updates the UI automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;drag--sort&quot;&gt;Drag &amp;#x26; Sort&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Enable reordering with a single property:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ItemDroppedCommand&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding ReorderCommand}&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableRoot&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Priority&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UseDragSort&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;True&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:LabelCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;High&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:LabelCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Medium&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:LabelCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Low&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableRoot&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Each cell gets up/down arrow controls.  The &lt;code dir=&quot;auto&quot;&gt;ItemDroppedCommand&lt;/code&gt; receives &lt;code dir=&quot;auto&quot;&gt;ItemDroppedEventArgs&lt;/code&gt; with the section, cell, and from/to indexes so you can update your backing data.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;radio-groups&quot;&gt;Radio Groups&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Radio selection is handled through an attached property that can scope to a section or the entire TableView:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Theme&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                 &lt;/span&gt;&lt;span&gt;tv:RadioCell.SelectedValue&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding SelectedTheme, Mode=TwoWay}&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:RadioCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Light&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Light&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:RadioCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Dark&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Dark&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:RadioCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;System&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;System&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Bind &lt;code dir=&quot;auto&quot;&gt;RadioCell.SelectedValue&lt;/code&gt; to your ViewModel and you get two-way radio group selection with no code-behind.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;full-page-picker&quot;&gt;Full-Page Picker&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;PickerCell&lt;/code&gt; navigates to a dedicated selection page — useful for long lists or multi-select scenarios:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:PickerCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Interests&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;ItemsSource&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding AllInterests}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;SelectedItems&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding SelectedInterests, Mode=TwoWay}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;SelectionMode&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Multiple&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;MaxSelectedNumber&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;5&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;UsePickToClose&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;True&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;UseAutoValueText&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;True&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;               &lt;/span&gt;&lt;span&gt;PageTitle&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Select Interests&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It auto-generates the value text from your selections, supports single and multi-select modes, and can auto-close when the max selection count is reached.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Install the NuGet package:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Maui.TableView&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Register in &lt;code dir=&quot;auto&quot;&gt;MauiProgram.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MauiApp.&lt;/span&gt;&lt;span&gt;CreateBuilder&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UseMauiApp&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UseShinyTableView&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Add the XAML namespace and start building:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ContentPage&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xmlns:tv&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://shiny.net/maui/tableview&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableRoot&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;General&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:SwitchCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Notifications&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                               &lt;/span&gt;&lt;span&gt;On&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding NotificationsEnabled, Mode=TwoWay}&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:EntryCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Name&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                              &lt;/span&gt;&lt;span&gt;ValueText&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding UserName, Mode=TwoWay}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                              &lt;/span&gt;&lt;span&gt;Placeholder&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Enter your name&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;tv:CommandCell&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;About&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                                &lt;/span&gt;&lt;span&gt;Command&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;{Binding AboutCommand}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                                &lt;/span&gt;&lt;span&gt;ShowArrow&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;True&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableSection&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableRoot&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;tv:TableView&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ContentPage&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;when-to-use-it&quot;&gt;When to Use It&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Settings and preferences pages&lt;/li&gt;
&lt;li&gt;Form-style data entry screens&lt;/li&gt;
&lt;li&gt;Profile editing UIs&lt;/li&gt;
&lt;li&gt;Any structured, section-based list with mixed control types&lt;/li&gt;
&lt;li&gt;Apps targeting iOS, Android, and Mac Catalyst from a single codebase&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not the best fit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long scrolling lists with hundreds or thousands of items (no virtualization)&lt;/li&gt;
&lt;li&gt;Data grids or spreadsheet-style layouts&lt;/li&gt;
&lt;li&gt;Chat interfaces or feed-style UIs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Maui.TableView&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Full documentation at &lt;a href=&quot;https://shinylib.net/tableview/&quot;&gt;shinylib.net/tableview&lt;/a&gt; and the &lt;a href=&quot;https://github.com/shinyorg/shinytableview&quot;&gt;GitHub repository&lt;/a&gt; has the complete source, sample app, and issue tracker.&lt;/p&gt;</content:encoded><category>TableView</category></item><item><title>Shiny Mediator &amp; AOT - Zero Reflection, Full Speed</title><link>https://www.shinylib.net/blog/2026/02/shinymediator-aot/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/02/shinymediator-aot/</guid><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Native AOT and trimming are no longer “nice to have” — they’re table stakes for modern .NET apps.  iOS has never allowed JIT.  Blazor WebAssembly ships every byte to the browser.  ASP.NET minimal APIs are racing toward sub-10ms cold starts.  If your library leans on reflection, you’re the bottleneck.&lt;/p&gt;
&lt;p&gt;Shiny Mediator took this personally.&lt;/p&gt;
&lt;p&gt;Starting in v5 and fully realized in v6, Shiny Mediator has waged a &lt;strong&gt;war on reflection&lt;/strong&gt;.  Every piece of runtime introspection has been replaced with compile-time source generation.  The result?  A mediator pipeline that is 100% AOT-safe, fully trimmable, and frankly… faster than it has any right to be.&lt;/p&gt;
&lt;p&gt;Let’s walk through every source generator and design decision that makes this possible.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;1-handler--middleware-registration--mediatorsingleton--mediatorscoped&quot;&gt;1. Handler &amp;#x26; Middleware Registration — &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;[MediatorScoped]&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The old way of registering handlers meant scanning assemblies, resolving open generics, and hoping the DI container could figure it all out at runtime.  That’s… not AOT-friendly.&lt;/p&gt;
&lt;p&gt;Shiny Mediator replaces all of that with two attributes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MediatorSingleton&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetUserHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetUserRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;UserResponse&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;UserResponse&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;GetUserRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// your handler logic&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;At compile time, the source generator discovers every class decorated with &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;[MediatorScoped]&lt;/code&gt; and emits all the DI registration code for you.  No assembly scanning.  No &lt;code dir=&quot;auto&quot;&gt;typeof()&lt;/code&gt; gymnastics.  No reflection.&lt;/p&gt;
&lt;p&gt;The generated code feeds into a &lt;strong&gt;module initializer registry&lt;/strong&gt; — a static registry that collects every handler and middleware registration across your entire solution.  To wire it all up:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x.&lt;/span&gt;&lt;span&gt;AddMediatorRegistry&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;One line.  Every handler.  Every middleware.  All generated at compile time.&lt;/p&gt;
&lt;p&gt;These attributes also handle middleware registration — so if you have a custom middleware class, slap &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; on it and it joins the party automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;2-request--stream-executors&quot;&gt;2. Request &amp;#x26; Stream Executors&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s a fun .NET limitation: you can’t easily resolve a generic type at runtime without knowing the type parameters at compile time.  Previous versions of Shiny Mediator used reflection to bridge that gap.  It worked, but it was slow and it was the single biggest AOT blocker.&lt;/p&gt;
&lt;p&gt;Starting in v5, source generators create &lt;strong&gt;typed executors&lt;/strong&gt; for every request and stream request in your project.  These executors know the exact types at compile time, so the mediator can dispatch directly without any &lt;code dir=&quot;auto&quot;&gt;MakeGenericType&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;Activator.CreateInstance&lt;/code&gt; shenanigans.&lt;/p&gt;
&lt;p&gt;You don’t need to do anything extra — if you’re using &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;[MediatorScoped]&lt;/code&gt;, the executor generation comes along for the ride.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;3-json-converter-source-generation&quot;&gt;3. JSON Converter Source Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AOT’s nemesis is &lt;code dir=&quot;auto&quot;&gt;System.Text.Json&lt;/code&gt; with reflection-based serialization.  The standard fix is &lt;code dir=&quot;auto&quot;&gt;JsonSerializerContext&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;[JsonSerializable]&lt;/code&gt; — but here’s the problem: &lt;strong&gt;you can’t chain source generators&lt;/strong&gt;.  Shiny Mediator’s source generator runs first, and &lt;code dir=&quot;auto&quot;&gt;System.Text.Json&lt;/code&gt;’s source generator can’t see the types that were just generated.&lt;/p&gt;
&lt;p&gt;So Shiny Mediator built its own JSON serialization source generator.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;for-http-contracts-openapi&quot;&gt;For HTTP Contracts (OpenAPI)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;When you generate HTTP clients from OpenAPI specs, just flip one switch:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MediatorHttp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Include&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;MyApi&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Uri&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://api.example.com/openapi.json&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Namespace&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;MyApp.Api&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;GenerateJsonConverters&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Visible&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;false&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Setting &lt;code dir=&quot;auto&quot;&gt;GenerateJsonConverters=&quot;true&quot;&lt;/code&gt; tells the source generator to emit high-performance, AOT-safe JSON converters for every contract and response type it produces.  No reflection.  No &lt;code dir=&quot;auto&quot;&gt;JsonSerializerContext&lt;/code&gt; registration.  The &lt;code dir=&quot;auto&quot;&gt;[JsonConverter]&lt;/code&gt; attribute is placed directly on each type.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;for-your-own-types&quot;&gt;For Your Own Types&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Got your own classes that need serialization — maybe for offline storage, caching, or custom contracts?  Use the &lt;code dir=&quot;auto&quot;&gt;[SourceGenerateJsonConverter]&lt;/code&gt; attribute:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;SourceGenerateJsonConverter&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WeatherForecast&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;City&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Temperature&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DateTime&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The class must be &lt;code dir=&quot;auto&quot;&gt;partial&lt;/code&gt; (the generator needs to attach code to it).  That’s it — you get a compile-time JSON converter without ever touching &lt;code dir=&quot;auto&quot;&gt;System.Text.Json&lt;/code&gt; source generation configuration.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;4-contract-key-source-generation&quot;&gt;4. Contract Key Source Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Middleware like caching, offline storage, and stream replay all need a &lt;strong&gt;key&lt;/strong&gt; to identify unique requests.  The “old school” way was implementing &lt;code dir=&quot;auto&quot;&gt;IContractKey&lt;/code&gt; on your contract:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Before — manual, tedious, error-prone&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SearchRequest&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SearchResult&lt;/span&gt;&lt;span&gt;&gt;, &lt;/span&gt;&lt;span&gt;IContractKey&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DateTime&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Since&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetKey&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;SearchRequest&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (Query &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) key &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$&quot;_&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (Page &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) key &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$&quot;_&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (Since &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) key &lt;/span&gt;&lt;span&gt;+=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$&quot;_&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;Since&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;yyyyMMdd&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; key;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Writing null checks and format strings for every property on every contract gets old fast.  And the default &lt;code dir=&quot;auto&quot;&gt;IContractKeyProvider&lt;/code&gt; used reflection to build keys when you didn’t implement the interface.&lt;/p&gt;
&lt;p&gt;The source-generated version:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// After — one attribute, zero reflection&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ContractKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;SearchRequest_{Query}_{Page}_{Since:yyyyMMdd}&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SearchRequest&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SearchResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DateTime&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Since&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The source generator handles null checks, &lt;code dir=&quot;auto&quot;&gt;ToString()&lt;/code&gt; calls, format strings — everything.  If a property is null, that portion of the key is replaced with an empty string.  The class must be &lt;code dir=&quot;auto&quot;&gt;partial&lt;/code&gt;, and you can use the same format specifiers you’d use in string interpolation.&lt;/p&gt;
&lt;p&gt;Leave the format string blank and it uses &lt;strong&gt;all public instance properties&lt;/strong&gt; automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;5-middleware-attribute-source-generation&quot;&gt;5. Middleware Attribute Source Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This one is subtle but critical.  Many of Shiny Mediator’s middleware components are driven by attributes on handler methods:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetProductHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetProductRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Cache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MaxAgeSeconds&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;OfflineAvailable&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;GetProductRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// fetch product from API&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Reading attributes from methods at runtime requires deep reflection — &lt;code dir=&quot;auto&quot;&gt;MethodInfo.GetCustomAttributes()&lt;/code&gt; and friends.  In an AOT world, that can fail silently or crash spectacularly.&lt;/p&gt;
&lt;p&gt;Shiny Mediator’s source generator scans handler methods at compile time, extracts every attribute that inherits from &lt;code dir=&quot;auto&quot;&gt;MediatorMiddlewareAttribute&lt;/code&gt;, and emits code that makes them available via &lt;code dir=&quot;auto&quot;&gt;context.GetHandlerAttribute&amp;#x3C;T&gt;()&lt;/code&gt; — no reflection needed at runtime.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your handler class must be &lt;code dir=&quot;auto&quot;&gt;partial&lt;/code&gt;&lt;/strong&gt; for this to work.  That’s the one rule.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;custom-middleware-attributes&quot;&gt;Custom Middleware Attributes&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Best of all, this works with your own attributes too.  Just inherit from &lt;code dir=&quot;auto&quot;&gt;MediatorMiddlewareAttribute&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuditLogAttribute&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;MediatorMiddlewareAttribute&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Category&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;General&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateOrderHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;CreateOrderRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Category&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Orders&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CreateOrderRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In your middleware — zero reflection&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuditLogMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;IRequestMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;Process&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RequestHandlerDelegate&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;next&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; context.&lt;/span&gt;&lt;span&gt;GetHandlerAttribute&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AuditLogAttribute&lt;/span&gt;&lt;span&gt;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (attr &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// log with attr.Category&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;next&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;6-openapi-http-client-generation&quot;&gt;6. OpenAPI HTTP Client Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The HTTP extension’s OpenAPI source generator is arguably the crown jewel of Shiny Mediator’s AOT story.  From a single OpenAPI spec, it generates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Request contracts&lt;/strong&gt; with proper HTTP verb, route, query, header, and body annotations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response types&lt;/strong&gt; matching the API schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A typed request handler&lt;/strong&gt; that uses &lt;code dir=&quot;auto&quot;&gt;HttpClient&lt;/code&gt; under the hood&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON converters&lt;/strong&gt; (when &lt;code dir=&quot;auto&quot;&gt;GenerateJsonConverters=&quot;true&quot;&lt;/code&gt;) for every generated type&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DI registration&lt;/strong&gt; via &lt;code dir=&quot;auto&quot;&gt;.AddGeneratedOpenApiClient()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All from a csproj item:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MediatorHttp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Include&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;PetStore&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Uri&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://petstore.swagger.io/v2/swagger.json&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Namespace&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;MyApp.PetStore&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;ContractPostfix&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;HttpRequest&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;GenerateJsonConverters&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Visible&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;false&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;AddMediatorRegistry&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;AddGeneratedOpenApiClient&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now every HTTP call flows through the mediator pipeline — which means caching, offline, validation, performance logging, and every other middleware you’ve configured &lt;strong&gt;automatically applies&lt;/strong&gt; to your API calls.  And it’s all AOT-safe because every type is known at compile time.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;7-aspnet-endpoint-source-generation&quot;&gt;7. ASP.NET Endpoint Source Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;On the server side, Shiny Mediator can source-generate minimal API endpoints directly from your handlers:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MediatorScoped&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateUserHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;CreateUserRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;UserResponse&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/api/users&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;UserResponse&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CreateUserRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// create user&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The source generator emits the &lt;code dir=&quot;auto&quot;&gt;app.MapPost(&quot;/api/users&quot;, ...)&lt;/code&gt; call and the DI wiring.  No controller classes.  No manual endpoint mapping.  Just your handler with an HTTP attribute and the generator does the rest.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-big-picture&quot;&gt;The Big Picture&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s what Shiny Mediator source-generates at compile time — and what it &lt;strong&gt;doesn’t&lt;/strong&gt; do at runtime:&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Feature&lt;/th&gt;&lt;th&gt;Compile Time (Source Gen)&lt;/th&gt;&lt;th&gt;Runtime (Reflection)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Handler DI Registration&lt;/td&gt;&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;[MediatorScoped]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;❌ No scanning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Request/Stream Executors&lt;/td&gt;&lt;td&gt;✅ Typed dispatch&lt;/td&gt;&lt;td&gt;❌ No &lt;code dir=&quot;auto&quot;&gt;MakeGenericType&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JSON Serialization&lt;/td&gt;&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;[SourceGenerateJsonConverter]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;❌ No reflection-based serializers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Contract Keys&lt;/td&gt;&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;[ContractKey]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;❌ No property reflection&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Middleware Attributes&lt;/td&gt;&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;[Cache]&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;[OfflineAvailable]&lt;/code&gt;, custom&lt;/td&gt;&lt;td&gt;❌ No &lt;code dir=&quot;auto&quot;&gt;GetCustomAttributes&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HTTP Clients&lt;/td&gt;&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;MediatorHttp&lt;/code&gt; MSBuild item&lt;/td&gt;&lt;td&gt;❌ No runtime proxy generation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ASP.NET Endpoints&lt;/td&gt;&lt;td&gt;✅ Handler method attributes&lt;/td&gt;&lt;td&gt;❌ No controller discovery&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The result is a mediator library that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ships on iOS&lt;/strong&gt; without fighting the linker&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runs in Blazor WASM&lt;/strong&gt; without bloating the download&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cold-starts fast&lt;/strong&gt; on ASP.NET because there’s nothing to scan or JIT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trims clean&lt;/strong&gt; because every code path is statically reachable&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;getting-started-with-aot&quot;&gt;Getting Started with AOT&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you’re already using Shiny Mediator, the path to full AOT is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make handler and contract classes &lt;code dir=&quot;auto&quot;&gt;partial&lt;/code&gt;&lt;/strong&gt; — the source generators need this&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;[MediatorScoped]&lt;/code&gt;&lt;/strong&gt; to your handlers and middleware&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code dir=&quot;auto&quot;&gt;[ContractKey]&lt;/code&gt;&lt;/strong&gt; instead of implementing &lt;code dir=&quot;auto&quot;&gt;IContractKey&lt;/code&gt; manually&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set &lt;code dir=&quot;auto&quot;&gt;GenerateJsonConverters=&quot;true&quot;&lt;/code&gt;&lt;/strong&gt; on your &lt;code dir=&quot;auto&quot;&gt;MediatorHttp&lt;/code&gt; items&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code dir=&quot;auto&quot;&gt;[SourceGenerateJsonConverter]&lt;/code&gt;&lt;/strong&gt; on custom types that need serialization&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Call &lt;code dir=&quot;auto&quot;&gt;x.AddMediatorRegistry()&lt;/code&gt;&lt;/strong&gt; in your startup instead of manual registration&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s it.  No reflection.  No runtime surprises.  Just compile-time confidence.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AOT and trimming aren’t just performance checkboxes — they’re the foundation of where .NET is heading.  Shiny Mediator has gone all-in on source generation to make sure you can take your mediator pipeline anywhere .NET runs: mobile, browser, server, and beyond.&lt;/p&gt;
&lt;p&gt;If reflection was the training wheels, source generation is the carbon fiber frame.  Time to ride.&lt;/p&gt;
&lt;p&gt;Check out the full &lt;a href=&quot;https://shinylib.net/mediator/sourcegeneration&quot;&gt;source generation docs&lt;/a&gt; and the &lt;a href=&quot;https://shinylib.net/mediator/extensions/http&quot;&gt;HTTP extension docs&lt;/a&gt; for all the details.&lt;/p&gt;</content:encoded><category>Mediator</category></item><item><title>What&apos;s New in Shiny Mediator 6</title><link>https://www.shinylib.net/blog/2026/02/shinymediator-whats-new-v6/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/02/shinymediator-whats-new-v6/</guid><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Shiny Mediator v6 has landed and it’s packed with some exciting new features and improvements. This release is a big step forward for the library, focusing on better source generation, AOT readiness, ASP.NET interop, and some slick new middleware.  Let’s dive in.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;net-8--10-minimum-targets&quot;&gt;.NET 8 &amp;#x26; 10 Minimum Targets&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The biggest (and breaking) change is that Shiny Mediator v6 now targets .NET 8 and .NET 10 as minimum versions.  This allows us to take advantage of the latest runtime features for performance, trimming, and AOT compilation.  If you’re still on older versions, you’ll need to stay on v5.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;revamped-http-client-source-generation&quot;&gt;Revamped HTTP Client Source Generation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The HTTP client source generation has been completely overhauled.  In v5, the OpenAPI generation would produce contracts, but you’d still need to wire up handlers and serialization yourself.  In v6, the source generator now produces &lt;strong&gt;everything&lt;/strong&gt; — handlers, contracts, JSON converters, and dependency injection registration.&lt;/p&gt;
&lt;p&gt;This is a breaking change because you’ll need to call a new registration method, but the setup through &lt;code dir=&quot;auto&quot;&gt;MediatorHttpItem&lt;/code&gt; in your csproj remains the same:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MediatorHttp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Include&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;OpenApiRemote&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Uri&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://yourapi.com/openapi.json&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Namespace&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;My.Namespace&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;ContractPostfix&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;HttpRequest&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;GenerateJsonConverters&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Visible&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;false&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And then in your startup:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x.&lt;/span&gt;&lt;span&gt;AddGeneratedOpenApiClient&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it.  Full HTTP client with AOT-safe serialization, middleware support, and zero boilerplate.&lt;/p&gt;
&lt;p&gt;Another nice improvement: if your OpenAPI spec doesn’t have an &lt;code dir=&quot;auto&quot;&gt;OperationId&lt;/code&gt;, the source generator now &lt;strong&gt;infers&lt;/strong&gt; a name using the HTTP verb and path.  For example, &lt;code dir=&quot;auto&quot;&gt;GET /user/list&lt;/code&gt; becomes &lt;code dir=&quot;auto&quot;&gt;GetUserList&lt;/code&gt;.  No more missing contracts because someone forgot to set an operation ID.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;directhttprequest-is-gone&quot;&gt;DirectHttpRequest is Gone&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;DirectHttpRequest&lt;/code&gt; type has been removed.  You can still create your own HTTP objects with the &lt;code dir=&quot;auto&quot;&gt;[Http]&lt;/code&gt; attribute which will source generate a handler for you.  This was cleaned up to reduce confusion between the direct approach and the more powerful generated approach.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;publishtobackground&quot;&gt;PublishToBackground&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One of the more subtle but impactful additions is &lt;code dir=&quot;auto&quot;&gt;PublishToBackground&lt;/code&gt; on &lt;code dir=&quot;auto&quot;&gt;IMediator&lt;/code&gt;.  If you’ve ever tried to fire-and-forget an event in ASP.NET, you’ve probably run into scoping issues where the &lt;code dir=&quot;auto&quot;&gt;HttpContext&lt;/code&gt; or scoped services get disposed before your event handlers finish.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;PublishToBackground&lt;/code&gt; solves this by spawning a &lt;strong&gt;new child scope&lt;/strong&gt;, so you can fire events without worrying about disposal:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyController&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;ControllerBase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;HttpPost&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;IActionResult&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;DoSomething&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// this returns immediately and the event handlers&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// run in a separate scope&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;PublishToBackground&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SomethingHappenedEvent&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Ok&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can still use the standard &lt;code dir=&quot;auto&quot;&gt;Publish&lt;/code&gt; to await all event handlers in the same scope.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;async-enumerable-responses--server-sent-events&quot;&gt;Async Enumerable Responses &amp;#x26; Server-Sent Events&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Stream requests now support &lt;code dir=&quot;auto&quot;&gt;IAsyncEnumerable&amp;#x3C;T&gt;&lt;/code&gt; responses that can be wired directly to &lt;strong&gt;Server-Sent Events&lt;/strong&gt; endpoints.  Just mark your contract with &lt;code dir=&quot;auto&quot;&gt;IServerSentEventsStream&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/api/sse&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ServerSentEventsRequest&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IStreamRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;EventItem&lt;/span&gt;&lt;span&gt;&gt;, &lt;/span&gt;&lt;span&gt;IServerSentEventsStream&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If your endpoint does &lt;strong&gt;not&lt;/strong&gt; support server-sent events, you can still use &lt;code dir=&quot;auto&quot;&gt;IStreamRequest&amp;#x3C;T&gt;&lt;/code&gt; — just drop the &lt;code dir=&quot;auto&quot;&gt;IServerSentEventsStream&lt;/code&gt; interface and you get a standard async enumerable response.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;event-throttling&quot;&gt;Event Throttling&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;code dir=&quot;auto&quot;&gt;[Throttle]&lt;/code&gt; attribute implements a &lt;strong&gt;debounce pattern&lt;/strong&gt; for event handlers.  This is huge for scenarios like search-as-you-type, sensor data processing, or any rapid-fire event where you only care about the latest value:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; cfg.&lt;/span&gt;&lt;span&gt;AddMediatorRegistry&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;AddThrottleEventMiddleware&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MediatorSingleton&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SearchHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SearchChangedEvent&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Throttle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;// wait 300ms after last event&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;SearchChangedEvent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@event&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// only fires once the user stops typing for 300ms&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;results&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; searchService.&lt;/span&gt;&lt;span&gt;Search&lt;/span&gt;&lt;span&gt;(@event.Query, ct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;How it works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When an event is published, a timer starts&lt;/li&gt;
&lt;li&gt;If the same event fires again before the timer expires, the timer resets and the previous event is discarded&lt;/li&gt;
&lt;li&gt;Only after the full delay with no new events does the handler execute with the latest data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your handler class must be &lt;code dir=&quot;auto&quot;&gt;partial&lt;/code&gt; for source generation to work.&lt;/p&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;Know what else is epic?  That &lt;code dir=&quot;auto&quot;&gt;[MediatorSingleton]&lt;/code&gt; &amp;#x26; &lt;code dir=&quot;auto&quot;&gt;[MediatorScoped]&lt;/code&gt; also work on middleware!&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;middleware-ordering&quot;&gt;Middleware Ordering&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Previously, middleware executed in DI registration order.  That’s fine for simple scenarios, but as your pipeline grows you need more control.  The new &lt;code dir=&quot;auto&quot;&gt;[MiddlewareOrder]&lt;/code&gt; attribute gives you explicit ordering:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MiddlewareOrder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;// runs first (outermost)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ValidationMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;IRequestMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt; { ... }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MiddlewareOrder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;// default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoggingMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;IRequestMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt; { ... }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MiddlewareOrder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;// runs last (closest to handler)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CachingMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;IRequestMiddleware&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TResult&lt;/span&gt;&lt;span&gt;&gt; { ... }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This produces: &lt;strong&gt;Validation → Logging → Caching → Handler → Caching → Logging → Validation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lower values run first.  If you don’t use the attribute, everything works exactly as before — it’s fully opt-in.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;http-response-cache-middleware&quot;&gt;HTTP Response Cache Middleware&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A new built-in middleware that caches HTTP responses based on &lt;code dir=&quot;auto&quot;&gt;CacheControl&lt;/code&gt; &lt;code dir=&quot;auto&quot;&gt;MaxAge&lt;/code&gt; headers.  If your API sends proper cache headers, Shiny Mediator will respect them automatically without any extra configuration on your part.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;additional-improvements--fixes&quot;&gt;Additional Improvements &amp;#x26; Fixes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Beyond the headline features, v6 includes a steady stream of refinements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EventStream redesigned&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;IMediator.EventStream&lt;/code&gt; was dropping events under load.  It’s been completely redesigned for speed and reliability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RuntimeEventRegister is now thread-safe&lt;/strong&gt; — dynamic event handler registration no longer has race conditions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BaseHttpRequestHandler now handles commands&lt;/strong&gt; — not just requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAPI source generation fixes&lt;/strong&gt; — proper URI path generation for PUT/POST, fixed nullable support, TimeSpan handling, and duplicate type issues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prism RegionNavigationCommand&lt;/strong&gt; — contributed by &lt;a href=&quot;https://github.com/codelisk&quot;&gt;codelisk&lt;/a&gt; for region-based navigation in Prism.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-tips&quot;&gt;Migration Tips&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you’re coming from v5, here’s the key things to watch for:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Update your target framework&lt;/strong&gt; to .NET 8 or .NET 10&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP client registration&lt;/strong&gt; — call the new &lt;code dir=&quot;auto&quot;&gt;AddGeneratedOpenApiClient()&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove DirectHttpRequest usages&lt;/strong&gt; — switch to &lt;code dir=&quot;auto&quot;&gt;[Http]&lt;/code&gt; attribute-based contracts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAPI contracts&lt;/strong&gt; may have slightly different names due to the automatic &lt;code dir=&quot;auto&quot;&gt;HttpRequest&lt;/code&gt; postfix&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Shiny Mediator v6 continues the push toward zero-boilerplate, AOT-ready mediation for .NET.  The revamped HTTP source generation alone is worth the upgrade — and features like event throttling and middleware ordering bring capabilities that would take significant effort to build yourself.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href=&quot;https://shinylib.net/mediator/&quot;&gt;full documentation&lt;/a&gt; and the &lt;a href=&quot;https://shinylib.net/release-notes/mediator/&quot;&gt;complete release notes&lt;/a&gt; for all the details.&lt;/p&gt;</content:encoded><category>Mediator</category></item><item><title>Shiny Mediator - Getting Started</title><link>https://www.shinylib.net/blog/2026/01/shinymediator-gettingstarted/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2026/01/shinymediator-gettingstarted/</guid><pubDate>Fri, 30 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mediator patterns have been getting a lot of attention lately, and for good reason. They help to decouple components in an application, making it easier to manage complexity and improve maintainability.  Mediator patterns also go by the names “Vertical slice architecture” and CQRS (Command Query Responsibility Segregation).
The big guy in .NET mediation is obviously &lt;a href=&quot;https://mediatr.io/&quot;&gt;MediatR&lt;/a&gt; by Jimmy Bogard.  It’s an amazing library that has been used by an absolute ton of applications.  It recently went to a paid model, which is understandable given the amount of work that goes into maintaining a library of that size.&lt;/p&gt;
&lt;p&gt;The reason for building Shiny Mediator was to create a mediation library, but make sure it works with apps built on platforms like .NET MAUI and Blazor WebAssembly while also including some more “batteries included” features.  This doesn’t mean I’ve neglected things like ASPNET support, but we’ll get to that in a future article.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-haters&quot;&gt;The Haters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Many engineers will call mediators “over engineering” or “an anti-pattern”.  I disagree.  In fact, I think mediators are one of the best patterns for building complex applications without going full bat stuff crazy with microservices out of the gate.  You can run in a monolith while still keeping things decoupled and manageable thereby making it easy to “SLICE” a piece out and move it to a microservice as your application traffic grows.
Another advantage of a mediation pattern is can remove the “dependency injection hell” of adding a services for things like logging, caching, and other cross cutting concerns.  Instead of having to add these services to every handler, you can just add them to the mediator pipeline, configure them or stick an attribute on a handler.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IDataService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ICacheManager&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;IConfiguration&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ILogger&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;logger&lt;/span&gt;&lt;span&gt;)  {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoadData&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.Data &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; cache.&lt;/span&gt;&lt;span&gt;TryGet&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;MyKey&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; data.&lt;/span&gt;&lt;span&gt;GetData&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The above code doesn’t look too bad, but&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Imagine you have 20 view models that all need these services.&lt;/li&gt;
&lt;li&gt;How do I make sure I’m grabbing from the same cache everytime or clearing it for that matter?&lt;/li&gt;
&lt;li&gt;Building cache keys can be a pain&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;ContractKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;{UserId}-{IsActive}&quot;&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetDataRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IsActive&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserId&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetDataResponse&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Cache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoadData&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetDataRequest&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.Data &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.Result;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Homegrown/built in mediation patterns are often added, but require you do the growing.  Middleware management is also not a simple process.  Creating keys for caching requests, another whamo of complexity.  These are all things solved by Shiny Mediator.
AI generated code solutions is another one I hear.  “Just use AI to generate the code for you”.  AI is great at boilerplate or when you give it a very specific input &amp;#x26; output, but architecture… it will throw “slop” at you.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;some-of-the-current-challenges&quot;&gt;Some of the Current Challenges&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;As with all patterns, there are some trade offs.  Mediators map objects to handlers (methods or controllers).  It is currently not tool friendly to find a corresponding handler for a contract, so you want to make sure you structure your solution and projects well to find
handlers without too much effort.  Another problem that can occur is middleware executes in a pipeline, so if you have a lot of middleware, you can end up with performance issues if you’re not careful about managing the middleware to ensure quick execution.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;experience-of-the-past&quot;&gt;Experience of the Past&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I’ve seen some large .NET MAUI applications
that had so many engineers working on them, there was constant “fire drills” (the running joke that came to be).  Team A would change something that would break Team B’s work.  Team C would have to make changes to Team A’s code to update navigation or data retrieval.&lt;br&gt;
Quite often, these breaking changes wouldn’t even be picked up until a regression test.&lt;/p&gt;
&lt;p&gt;In this post, we’ll explore how to get started with Shiny Mediator, a library that implements the Mediator pattern with a focus on apps written with .NET.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-are-some-of-the-batteries-included&quot;&gt;What are some of the “batteries included”?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most mediator libraries hand you a pipe and say “good luck”.  Shiny Mediator hands you a pipe, a toolbox, a hard hat, and a coffee.  Here’s what’s in the box:&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;offline-mode&quot;&gt;Offline Mode&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Your users don’t always have internet. Shocking, I know. But building offline support from scratch is the kind of soul-crushing work that makes devs question their career choices. With Shiny Mediator, slap an attribute on your handler and you’re done:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetOrdersHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetOrdersRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;IReadOnlyList&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;OfflineAvailable&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;IReadOnlyList&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;GetOrdersRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// When online: calls your API, stores the result&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// When offline: returns the last stored result&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// You did nothing. You&apos;re welcome.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; api.&lt;/span&gt;&lt;span&gt;GetOrders&lt;/span&gt;&lt;span&gt;(ct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can also configure it via &lt;code dir=&quot;auto&quot;&gt;appsettings.json&lt;/code&gt; if attributes aren’t your thing:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;Mediator&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Offline&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;MyNamespace.GetOrdersRequest&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Want to know if the data came from the offline store?  The context tells you:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetOrdersRequest&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;offline&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.Context.&lt;/span&gt;&lt;span&gt;Offline&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (offline &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// data is from offline store&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// offline.Timestamp tells you WHEN it was stored&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// maybe show a &quot;stale data&quot; indicator to the user&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;caching&quot;&gt;Caching&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Caching is one of the two hardest problems in computer science (the other being naming things and off-by-one errors).  Shiny Mediator makes it embarrassingly easy:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetProductsHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetProductsRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Cache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AbsoluteExpirationSeconds&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;SlidingExpirationSeconds&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;GetProductsRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// This only runs when cache misses.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// No cache key management. No IMemoryCache injection. No tears.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;GetProducts&lt;/span&gt;&lt;span&gt;(ct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Setup is one line:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x.&lt;/span&gt;&lt;span&gt;AddMemoryCaching&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Need a cache that survives app restarts?  The MAUI and Uno extensions have a &lt;strong&gt;persistent cache&lt;/strong&gt; that writes to disk.  Same attribute, same config — it just doesn’t evaporate when the user kills your app:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x.&lt;/span&gt;&lt;span&gt;AddMauiPersistentCache&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can also force a cache refresh when you need fresh data (pull-to-refresh, anyone?):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetProductsRequest&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CancellationToken.None,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; ctx.&lt;/span&gt;&lt;span&gt;ForceCacheRefresh&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// response.Result is guaranteed fresh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;validation&quot;&gt;Validation&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Validation: where developers go to argue about whether to throw exceptions or return error objects.  We support both.  Pick your poison.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data Annotations&lt;/strong&gt; (built-in, no extra package):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; cfg.&lt;/span&gt;&lt;span&gt;AddDataAnnotations&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Validate&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateUserCommand&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ICommand&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Required&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Range&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;150&lt;/span&gt;&lt;span&gt;)] &lt;/span&gt;&lt;span&gt;// optimistic about human lifespans&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Age&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;FluentValidation&lt;/strong&gt; (for the overachievers):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; cfg.&lt;/span&gt;&lt;span&gt;AddFluentValidation&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Validate&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateUserCommand&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ICommand&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateUserValidator&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;AbstractValidator&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;CreateUserCommand&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateUserValidator&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;RuleFor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x.Name).&lt;/span&gt;&lt;span&gt;NotEmpty&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;WithMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;A user needs a name. Even &apos;Bob&apos; will do.&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Validation runs &lt;em&gt;before&lt;/em&gt; your handler ever sees the request.  Invalid data never touches your business logic.  It’s like a bouncer for your code.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;performance-logging&quot;&gt;Performance Logging&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Want to know which handler is being a lazy bum? Performance logging middleware tracks execution time through built-in diagnostics via &lt;code dir=&quot;auto&quot;&gt;Microsoft.Extensions.Diagnostics&lt;/code&gt;.  No extra setup, no third-party APM required.  Your handlers are already emitting telemetry — you just need to listen.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;http-client-generation&quot;&gt;HTTP Client Generation&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This one deserves its own blog post (coming soon), but the short version: point Shiny Mediator at your OpenAPI spec and it generates contracts, handlers, JSON converters, and DI registration.  No HttpClientFactory plumbing. No System.Text.Json serialization contexts.  No Polly setup.  Just:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MediatorHttp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Include&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;MyApi&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Uri&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://myapi.com/openapi.json&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Namespace&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;MyApp.Api&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;ContractPostfix&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;HttpRequest&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;GenerateJsonConverters&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span&gt;Visible&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;false&quot;&lt;/span&gt;&lt;span&gt; /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ItemGroup&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; x.&lt;/span&gt;&lt;span&gt;AddGeneratedOpenApiClient&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Now just use it like any other mediator request&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetUsersHttpRequest&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;All your middleware (caching, offline, validation, resilience) works with HTTP requests too.  One attribute to cache API calls.  One attribute for offline fallback.  Your API client just became the most resilient thing in your entire codebase — and you wrote zero infrastructure code.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Enough talk.  Let’s build something.  I’ll walk you through a .NET MAUI setup since that’s where Shiny Mediator really flexes, but this works just as well with Blazor, ASP.NET, or plain old console apps (we don’t judge).&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;step-1-install-the-packages&quot;&gt;Step 1: Install the packages&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Mediator&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dotnet&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny.Mediator.Maui&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Blazor, swap &lt;code dir=&quot;auto&quot;&gt;Shiny.Mediator.Maui&lt;/code&gt; for &lt;code dir=&quot;auto&quot;&gt;Shiny.Mediator.Blazor&lt;/code&gt;.  For ASP.NET or console apps, just &lt;code dir=&quot;auto&quot;&gt;Shiny.Mediator&lt;/code&gt; on its own is fine.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;step-2-define-your-contracts&quot;&gt;Step 2: Define your contracts&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Contracts are just plain C# records or classes that implement one of the mediator interfaces.  Think of them as the “what” — what data goes in, what data comes out.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Mediator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// A command - fire and forget, no return value&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateTodoCommand&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Title&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;ICommand&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// A request - send something in, get something back&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetTodosRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IncludeCompleted&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;span&gt;&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// An event - broadcast to anyone who&apos;s listening&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TodoCreatedEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Item&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;IEvent&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;step-3-create-your-handlers&quot;&gt;Step 3: Create your handlers&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Handlers are where the actual work happens.  One handler per command/request, but you can have multiple event handlers.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;using&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Shiny&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Mediator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MediatorSingleton&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;// source generator handles DI registration for you&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateTodoHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;ICommandHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;CreateTodoCommand&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMyDatabase&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateTodoHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMyDatabase&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.db &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; db;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.mediator &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mediator;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CreateTodoCommand&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;span&gt;(command.Title, command.Description);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;Insert&lt;/span&gt;&lt;span&gt;(item, ct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// broadcast that a todo was created&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Publish&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TodoCreatedEvent&lt;/span&gt;&lt;span&gt;(item));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;MediatorSingleton&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetTodosHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;GetTodosRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;span&gt;&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMyDatabase&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetTodosHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMyDatabase&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.db &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; db;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;Cache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AbsoluteExpirationSeconds&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;120&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;OfflineAvailable&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;span&gt;&gt;&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;GetTodosRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// cached for 2 minutes AND available offline&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// two attributes, zero infrastructure code, infinite smugness&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;GetTodos&lt;/span&gt;&lt;span&gt;(request.IncludeCompleted, ct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;step-4-wire-it-up&quot;&gt;Step 4: Wire it up&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MauiProgram&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MauiApp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateMauiApp&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MauiApp&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CreateBuilder&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UseMauiApp&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddShinyMediator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;UseMaui&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;AddMemoryCaching&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;x.&lt;/span&gt;&lt;span&gt;AddDataAnnotations&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// let the source generator register everything&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;builder.Services.&lt;/span&gt;&lt;span&gt;AddMediatorRegistry&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; builder.&lt;/span&gt;&lt;span&gt;Build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;step-5-use-it&quot;&gt;Step 5: Use it&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TodoListViewModel&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;BaseViewModel&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;IEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;TodoCreatedEvent&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TodoListViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.mediator &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mediator;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoadTodos&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// one line. cached. offline-available. validated.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetTodosRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IncludeCompleted&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.Todos &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.Result;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateTodo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CreateTodoCommand&lt;/span&gt;&lt;span&gt;(title, description));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// don&apos;t reload manually - the event handler below will fire&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// this fires automatically when TodoCreatedEvent is published&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// no subscription. no unsubscription. no memory leaks. no drama.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;TodoCreatedEvent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@event&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;IMediatorContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;context&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoadTodos&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Notice that the ViewModel implements &lt;code dir=&quot;auto&quot;&gt;IEventHandler&amp;#x3C;TodoCreatedEvent&gt;&lt;/code&gt; directly.  With the MAUI extension, your ViewModels and Pages automatically participate in event broadcasting without being registered in DI.  When the page is popped from navigation, it stops receiving events.  No &lt;code dir=&quot;auto&quot;&gt;MessagingCenter.Unsubscribe&lt;/code&gt; nightmares.  No &lt;code dir=&quot;auto&quot;&gt;WeakReferenceMessenger&lt;/code&gt; gymnastics.  It just works.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-the-road-ahead&quot;&gt;What’s the Road Ahead&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Shiny Mediator has come a long way from its v1 days.  We’re now on v6 with full AOT and trimming support baked into the source generators.  So what’s next?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deeper ASP.NET integration&lt;/strong&gt; — we’ve already got minimal API endpoint generation from handlers, server-sent events from stream requests, and HTTP response caching middleware.  Expect more first-class server scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event throttling &amp;#x26; middleware ordering&lt;/strong&gt; — both landed in v6.  &lt;code dir=&quot;auto&quot;&gt;[Throttle(300)]&lt;/code&gt; on your event handlers gives you debounce for free (goodbye, search-as-you-type headaches).  &lt;code dir=&quot;auto&quot;&gt;[MiddlewareOrder]&lt;/code&gt; gives you explicit pipeline control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better tooling&lt;/strong&gt; — finding the handler for a contract is still a navigation exercise.  We’re exploring IDE integrations to make this seamless.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More source generation&lt;/strong&gt; — the goal is zero reflection, zero runtime surprises.  If a handler is missing or misconfigured, you should know at compile time, not when your app blows up in production at 2 AM on a Saturday.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community contributions&lt;/strong&gt; — shout out to &lt;a href=&quot;https://github.com/codelisk&quot;&gt;codelisk&lt;/a&gt; for Prism region navigation and &lt;a href=&quot;https://github.com/JeremyBP&quot;&gt;JeremyBP&lt;/a&gt; for MAUI modal stack iteration.  Keep ‘em coming.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Shiny Mediator isn’t trying to replace MediatR.  It’s trying to be the mediator that gives app developers — especially those on MAUI, Blazor, and Uno — a batteries-included experience that “just works” out of the box.  Caching, offline, validation, HTTP clients, event broadcasting, source generated registration… all of it designed so you can focus on your actual business logic instead of plumbing.&lt;/p&gt;
&lt;p&gt;Give it a spin.  Check out the &lt;a href=&quot;https://shinylib.net/mediator/&quot;&gt;full docs&lt;/a&gt;, play with the &lt;a href=&quot;https://github.com/shinyorg/mediatorsample&quot;&gt;sample apps&lt;/a&gt;, and come yell at me on &lt;a href=&quot;https://bsky.app/profile/shinylib.net&quot;&gt;BlueSky&lt;/a&gt; if something doesn’t work.&lt;/p&gt;
&lt;p&gt;Happy coding.  May your caches always be warm and your pipelines always be clean.&lt;/p&gt;
&lt;p&gt;Source generation and full blown AOT.  AOT is becoming such an important part of .NET, especially with .NET MAUI and Blazor.  We’re working hard on a version 5 release that will be fully AOT compliant out of the box!&lt;/p&gt;</content:encoded><category>Mediator</category></item><item><title>Mediator v2.0</title><link>https://www.shinylib.net/blog/2024/09/mediator2/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2024/09/mediator2/</guid><pubDate>Sun, 22 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h1 id=&quot;the-road-here&quot;&gt;The Road Here&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;Only a little over 3 months, I released v1.0 of Mediator.  Since then, I’ve been adding features as fast as they come to mind.  The focus on making the Mediator
pattern upfront for apps (in my opinion) has been a huge success.  I’ve been using it in my MAUI apps for offline, caching, &amp;#x26; resiliency middleware.  The amount of time
it saves me is pretty crazy.&lt;/p&gt;
&lt;p&gt;Since version the v1 release, I’ve been adding a ton of features including an &lt;a href=&quot;https://www.shinylib.net/docs/client/mediator/extensions/aspnet&quot;&gt;ASP.NET extension&lt;/a&gt; for HTTP endpoints straight to Mediator request handlers.  Quite recently - I found a lot of mediated calls would end up just wrapping HTTP calls done with Refit or Kiota.  I decided to add my
own source generator to deal with this.  Check out the &lt;a href=&quot;https://www.shinylib.net/docs/client/mediator/extensions/http&quot;&gt;HTTP Extension&lt;/a&gt; for more on this.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-new-in-20&quot;&gt;What’s New in 2.0&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So what does 2.0 bring that requires a major version bump?  The first major feature is that I’ve brought the offline, stream replay, and user error notification middleware to Blazor webassembly.  This middleware was already
present in MAUI, but Blazor needed a “connectivity” &amp;#x26; “storage” service to match MAUI.&lt;/p&gt;
&lt;p&gt;The second major feature is that I’ve been moving a lot of the middleware to be configured via Microsoft.Extensions.Configuration.  This allows you to configure
middleware and handlers in a global way and without polluting your code with attributes everywhere.  Below is an example of all configuration we offer now:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;Mediator&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Http&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;My.Namespace.Contract&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://shinylib.net/newbase&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;My.Namespace.*&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;https://shinylib.net/therestofhtenamespace&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://shinylib.net/everythingelse&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Performance&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;ErrorThresholdMilliseconds&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Offline&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;Sample.Handlers.OfflineRequestHandler&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;AvailableAcrossSessions&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;ReplayStream&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;Sample.Handlers.MyRequest&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;AvailableAcrossSessions&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;TimerRefresh&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;My.Contacts.HttpData&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;IntervalSeconds&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Resilience&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;My.Namespace.ResilientContract&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;RetryCount&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;RetryDelay&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Cache&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;My.Contacts.*&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;Priority&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;High&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;AbsoluteExpirationSeconds&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;SlidingExpirationSeconds&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;UserErrorNotifications&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;&quot;Title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;ERROR&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;&quot;Message&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;Failed to do something&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;fr-CA&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;&quot;Title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;ERREUR&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;&quot;Message&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;Échec de faire quelque chose&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;So let’s unpack this.  The first thing to notice is that we have ”*” to “glob” a namespace or ALL calls.  If you want to attack a specific contract, just fully label it.
We will find the nearest namespace to your contract before giving up as “not enabled” for a feature to be disabled on a contract.&lt;/p&gt;
&lt;aside aria-label=&quot;Note&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Note&lt;/p&gt;&lt;div&gt;&lt;p&gt;Contracts with attributes still work as before, but they are treated as “secondary” to configuration.  Also note, that we have removed a couple of attributes in this release&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I’m pretty happy with this release, but I still have a ton of ideas.  All the stuff you can do around the handlers with middleware is really exciting.  Have an idea for the mediator,
head on over to &lt;a href=&quot;https://github.com/shinyorg/mediator&quot;&gt;GitHub&lt;/a&gt; and add a feature request.  I’m always looking for new ideas.&lt;/p&gt;</content:encoded><category>Release</category><category>Mediator</category></item><item><title>Mediator v1.0</title><link>https://www.shinylib.net/blog/2024/06/mediator1/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2024/06/mediator1/</guid><pubDate>Mon, 10 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;what-is-it&quot;&gt;What is it?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Shiny Mediator is something new I’ve been working on.  I love Jimmy Bogard’s MediatR library on the server, but I just couldn’t
get it to fit the way I wanted for Apps… especially Blazor &amp;#x26; .NET MAUI apps.&lt;/p&gt;
&lt;p&gt;What is a mediator? It’s a small in-process version of a message bus.  MAUI has MessagingCenter, the Community toolkit has the weak message center, and Prism
offers an event aggregator.  They’re all great, but they lack in areas that I want “more”.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If an event errors, the whole chain dies in the publish&lt;/li&gt;
&lt;li&gt;Events are fired in a foreach (mostly)&lt;/li&gt;
&lt;li&gt;You have to tie into them and unsubscribe from them or you can leak memory&lt;/li&gt;
&lt;li&gt;They don’t provide the concept of command or request/response models (a command can only be responded to by a single handler)&lt;/li&gt;
&lt;li&gt;They don’t provide any sort of middleware (pre &amp;#x26; post handling)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sure - they aren’t geared for these things, but the question is “what is”?&lt;/p&gt;
&lt;p&gt;Some might say “this is overengineering” or “too complex”. I would counter that comment by saying that this can actually simplify your architecture
overall by removing a lot of complex plumbing around services, references, and navigation.&lt;/p&gt;
&lt;p&gt;Let’s go over some of the problems that we use mediator to solve within our apps&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-does-it-solve&quot;&gt;What Does It Solve&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We believe that Shiny Mediator is the answer to these problems.  It’s a simple, yet powerful library that allows you to create a mediator in your app&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;problem-1---service--reference-hell&quot;&gt;Problem #1 - Service &amp;#x26; Reference Hell&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Does this look familiar to you?&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IConnectivity&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;conn&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IDataService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IAuthService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IDialogsService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dialogs&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ILogger&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;logger&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;try {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (conn.IsConnected)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;myData&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; data.&lt;/span&gt;&lt;span&gt;GetDataRequest&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;else {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dialogs.&lt;/span&gt;&lt;span&gt;Show&lt;/span&gt;&lt;span&gt;(&quot;&lt;/span&gt;&lt;span&gt;No&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Connection&lt;/span&gt;&lt;span&gt;&quot;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// cache?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ex&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dialogs.&lt;/span&gt;&lt;span&gt;Show&lt;/span&gt;&lt;span&gt;(ex.Message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;logger.&lt;/span&gt;&lt;span&gt;LogError&lt;/span&gt;&lt;span&gt;(ex);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;With a bit of our middleware and some events, you can get here:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;IEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ConnectivityChangedEvent&lt;/span&gt;&lt;span&gt;&gt;, &lt;/span&gt;&lt;span&gt;IEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AuthChangedEvent&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;myData&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GetDataRequest&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// logging, exception handling, offline caching can all be bundle into one nice clean call without the need for coupling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;problem-2---messages-everywhere--leaks&quot;&gt;Problem #2 - Messages EVERYWHERE (+ Leaks)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Do you use the MessagingCenter in Xamarin.Forms?  It’s a great tool, but it can lead to some memory leaks if you’re not careful.  It also doesn’t have
a pipeline, so any errors in any of the responders will crash the entire chain.  It doesn’t have a request/response style setup (not that it was meant for it), but
this means you still require other services.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MessagingCenter.&lt;/span&gt;&lt;span&gt;Subscribe&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SomeEvent1&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;@event&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// do something&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MessagingCenter.&lt;/span&gt;&lt;span&gt;Subscribe&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SomeEvent2&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;@event&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// do something&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MessagingCenter.&lt;/span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SomeEvent1&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MessagingCenter.&lt;/span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SomeEvent2&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// and don&apos;t forget to unsubscribe&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MessagingCenter.&lt;/span&gt;&lt;span&gt;Unsubscribe&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SomeEvent1&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MessagingCenter.&lt;/span&gt;&lt;span&gt;Unsubscribe&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SomeEvent2&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Let’s take a look at our mediator in action for this scenarios&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SomeEvent1&lt;/span&gt;&lt;span&gt;&gt;, &lt;/span&gt;&lt;span&gt;IEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;SomeEvent2&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// no need to unsubscribe&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mediator.&lt;/span&gt;&lt;span&gt;Publish&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SomeEvent1&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mediator.&lt;/span&gt;&lt;span&gt;Publish&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SomeEvent2&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;problem-3---strongly-typed-navigation-with-strongly-typed-arguments&quot;&gt;Problem #3 - Strongly Typed Navigation with Strongly Typed Arguments&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Our amazing friends over in Prism offer the “best in class” MVVM framework.  We’ll them upsell you beyond that, but one
of their amazing features is ‘Modules’.  Modules help break up your navigation registration, services, etc.&lt;/p&gt;
&lt;p&gt;What they don’t solve is providing a strongly typed nature for this stuff (not their job though).  We think we can help
addon to their beautiful solution.&lt;/p&gt;
&lt;p&gt;A normal call to a navigation service might look like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;_navigationService.&lt;/span&gt;&lt;span&gt;NavigateAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;MyPage&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NavigationParameters&lt;/span&gt;&lt;span&gt; { { &lt;/span&gt;&lt;span&gt;&quot;MyArg&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;MyValue&quot;&lt;/span&gt;&lt;span&gt; } });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is great.  It works, but I don’t know the type OR argument requirements of “MyPage” without going to look it up.  In a small project
with a small dev team, this is fine.  In a large project with a large dev team, this can be difficult.&lt;/p&gt;
&lt;p&gt;Through our Shiny.Framework library we offer a GlobalNavigationService that can be used to navigate to any page in your app from anywhere, however,
for the nature of this example, we’ll pass our navigation service FROM our viewmodel through the mediator request to ensure proper scope.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;record&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyPageNavigatonRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;INavigationService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;navigator&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyArg&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;IRequest&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyPageNavigationHandler&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;IRequestHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyPageNavigatonRequest&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Task&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MyPageNavigatonRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;CancellationToken&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cancellationToken&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; request.navigator.&lt;/span&gt;&lt;span&gt;NavigateAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;MyPage&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NavigationParameters&lt;/span&gt;&lt;span&gt; { { &lt;/span&gt;&lt;span&gt;&quot;MyArg&quot;&lt;/span&gt;&lt;span&gt;, request.MyArg } });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now, in your viewmodel, you can do this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre dir=&quot;ltr&quot;&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyViewModel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;IMediator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mediator&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mediator.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyPageNavigatonRequest&lt;/span&gt;&lt;span&gt;(_navigationService, &lt;/span&gt;&lt;span&gt;&quot;MyValue&quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Strongly typed.  No page required page knowledge from the module upfront.  The other dev team of the module can define HOW things work.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Lastly, we offer some absolutely epic middleware that you can use to do things like logging, caching, error handling, etc.&lt;br&gt;
Check out our &lt;a href=&quot;https://www.shinylib.net/client/mediator/middleware&quot;&gt;middleware documentation for more information&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Check out our overall &lt;a href=&quot;https://www.shinylib.net/client/mediator&quot;&gt;documentation&lt;/a&gt; for more information on how to use the mediator in your app.  We think you’ll love it!&lt;/p&gt;</content:encoded><category>Release</category><category>Mediator</category></item><item><title>April Mad Releases</title><link>https://www.shinylib.net/blog/2024/04/april2024/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2024/04/april2024/</guid><pubDate>Mon, 08 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;shiny-v33&quot;&gt;Shiny v3.3&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This release was mainly a large bugfixing release with some cool additions around Push Notifications&lt;/p&gt;
&lt;p&gt;However, we did decide to remove two modules from the Shiny offering&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shiny.Logging.AppCenter - this was an easy one.  Microsoft is shutting down AppCenter in 2025.&lt;/li&gt;
&lt;li&gt;Shiny.SpeechRecognition - this was a tough one.  The plugin was never really that great and it was a pain to maintain.  The MAUI community toolkit recently released a plugin here that you can use.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ve improved the push delegate to handle MORE native stuff, more events like UnRegister to centralize your registration &amp;#x26; now the unregistration process.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.shinylib.net/release-notes/client/v30/&quot;&gt;Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;apple-privacy-info---may-1-2024-deadline-is-approaching&quot;&gt;Apple Privacy Info - May 1, 2024 Deadline is Approaching&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Apple has a new privacy requirement that you must disclose what you are doing with the user’s data.  This is a requirement for all apps in the App Store.  The Shiny templates have been updated to include
a new file under the iOS platform that you can fill out to help you comply with this requirement.&lt;/p&gt;
&lt;p&gt;We’ve introduced two “guestimate” type helpers to get users rolling.  Our Shiny Templates and our &lt;a href=&quot;https://www.shinylib.net/appbuilder/&quot;&gt;App Builder&lt;/a&gt; tool will help you generate these files.  We generate what most user app requirements
will be in terms of user identity info, as well as all of the necessary Shiny &amp;#x26; .NET BCL requirements.  We also insert any necessary location permissions if you select one of our location based components.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;shiny-templates-v25&quot;&gt;Shiny Templates v2.5&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The templates have been great for Shiny discovery.  Wiring up all of the permissions with Android &amp;#x26; iOS is difficult.  I find that I’m constantly using
&lt;a href=&quot;https://www.shinylib.net/appbuilder/&quot;&gt;App Builder&lt;/a&gt; here on shinylib.net to generate them all for me because I constantly forget them all.  Cutting a new proof-of-concept or test with
Shiny or any of the whack of 3rd party libraries that are included with the library is a breeze.&lt;/p&gt;
&lt;p&gt;This release got the following love:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Removal of AppCenter - maybe this is the opposite of love - but don’t use it since it is officially done in 2025&lt;/li&gt;
&lt;li&gt;Updated to the latest versions of many nuget packages&lt;/li&gt;
&lt;li&gt;Updated some of the Shiny stuff to 3.3&lt;/li&gt;
&lt;li&gt;NEW base template for the upcoming Apple Privacy Manifests under Platforms/iOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The MAUI Project template is so large that it takes 3 screenshots to get it all.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.shinylib.net/images/template_1.png&quot; alt=&quot;1&quot;&gt;
&lt;img src=&quot;https://www.shinylib.net/images/template_2.png&quot; alt=&quot;2&quot;&gt;
&lt;img src=&quot;https://www.shinylib.net/images/template_3.png&quot; alt=&quot;3&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shinyorg/templates&quot;&gt;Check It Out&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;push-notification-end-to-end-tester-for-azure-notification-hubs--firebase&quot;&gt;Push Notification End-to-End Tester for Azure Notification Hubs &amp;#x26; Firebase&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Setting up push notifications with Apple &amp;#x26; Google can be a real pain. This tool will help you test your setup end-to-end.  It’s a
simple mobile app that will send a push notification to your app using Azure Notification Hubs or Firebase Cloud Messaging.  It’s a great way to test your setup without having to write a bunch of code.&lt;/p&gt;
&lt;p&gt;It isn’t pretty, but it does a good job of helping you test your setup without having to wire up a backend or write a bunch of code.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shinyorg/pushtester&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;firebase---specifically-ios&quot;&gt;Firebase - Specifically iOS&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;There is work being done to create fresh new slim bindings for Firebase on iOS.  I’m working with Jon Dick on some SLIM Bindings over at &lt;a href=&quot;https://github.com/Redth/DotNet.Platform.SlimBindings&quot;&gt;https://github.com/Redth/DotNet.Platform.SlimBindings&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Slim bindings are revolutionary by any means, but they allow you to control the native API surface that you have to bind to.  With Swift becoming increasingly popular and not having a true native binding solution with
.NET at this time, Slim bindings allow us to control this narrative by building swift code, but still marking the code properly with objective-C headers to be able to bind within .NET&lt;/p&gt;
&lt;p&gt;If you’re a user of Shiny.Push.FirebaseMessaging, the good news is that there will be a direct nuget package update in v3 that will allow you to continue using Firebase messaging on iOS without any other code updates.  This update
for Shiny will be available long before the Firebase June 2024 deadline&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;shiny-40-roadmap&quot;&gt;Shiny 4.0 Roadmap&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We are starting to plan out Shiny 4.0.  We are looking for feedback on what you would like to see in the next major version of Shiny.&lt;/p&gt;
&lt;p&gt;Currently on the roadmap are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simplifying the Job framework&lt;/li&gt;
&lt;li&gt;Allow for more native configuration of Push &amp;#x26; Local Notifications&lt;/li&gt;
&lt;li&gt;More BLE methods to help with some of the fun edge cases that have been rolling in&lt;/li&gt;
&lt;li&gt;Removing the old Xamarin/Netstandard targets.  This will help with new targets moving forward&lt;/li&gt;
&lt;li&gt;Continue to improve the hosting model to work with other platform vendors like Uno&lt;/li&gt;
&lt;li&gt;Trying to get RX out of the core for those that don’t want the fat (MAYBE)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it’s a “smaller” major release.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;bonus---acr-user-dialogs&quot;&gt;Bonus - ACR User Dialogs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;There are still people using my old dinosaur of a package, so I updated it to the latest AndroidX stuff and .NET 8.&lt;br&gt;
Unfortunately, classic Xamarin and netstandard targets are now gone, but at least it pushes the needle forward if you’re still using
this package.&lt;/p&gt;
&lt;p&gt;We may introduce Windows support back into the mix in the future, but it’s not a priority right now.&lt;/p&gt;
&lt;a href=&quot;https://www.nuget.org/packages/Acr.UserDialogs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://img.shields.io/nuget/v/Acr.UserDialogs?style=for-the-badge&amp;#x26;logo=nuget&quot; alt=&quot;NuGet package Acr.UserDialogs&quot;&gt;&lt;/a&gt;</content:encoded><category>Release</category></item><item><title>Version 3.2 Release</title><link>https://www.shinylib.net/blog/2023/12/v32/</link><guid isPermaLink="true">https://www.shinylib.net/blog/2023/12/v32/</guid><pubDate>Sun, 10 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today, we released version 3.2 of Shiny that targets .NET 8 along with Classic Xamarin targets.  We decided to move with the supported version of MAUI.  This also makes things easier
for specific support targets with AndroidX which can be very difficult to multitarget with.&lt;/p&gt;
&lt;p&gt;This release also includes the ability to check for the current setup permission without requesting from the permission from the user.  This is surprisingly difficult to implement for all modules, so for
now, the following libraries have this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shiny.Locations / IGeofenceManager.CurrentAccess&lt;/li&gt;
&lt;li&gt;Shiny.Locations / IGeofenceManager.GetCurrentAccess(GpsRequest)&lt;/li&gt;
&lt;li&gt;Shiny.BluetoothLE / IPeripheralManager.CurrentAccess&lt;/li&gt;
&lt;li&gt;Shiny.BluetoothLE.Hosting / IBleHostingManager.CurrentAdvertisingAccess &amp;#x26; IBleHostingManager.CurrentGattAccess&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are other minor fixes in this release.  Be sure to checkout our &lt;a href=&quot;https://www.shinylib.net/release-notes/client/v30&quot;&gt;full release notes&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Release</category></item></channel></rss>